diff --git a/bun.lock b/bun.lock
index 5b8d3afe5..3dda9a855 100644
--- a/bun.lock
+++ b/bun.lock
@@ -44,17 +44,17 @@
"zod": "^4.4.3",
},
"optionalDependencies": {
- "oh-my-opencode-darwin-arm64": "4.5.1",
- "oh-my-opencode-darwin-x64": "4.5.1",
- "oh-my-opencode-darwin-x64-baseline": "4.5.1",
- "oh-my-opencode-linux-arm64": "4.5.1",
- "oh-my-opencode-linux-arm64-musl": "4.5.1",
- "oh-my-opencode-linux-x64": "4.5.1",
- "oh-my-opencode-linux-x64-baseline": "4.5.1",
- "oh-my-opencode-linux-x64-musl": "4.5.1",
- "oh-my-opencode-linux-x64-musl-baseline": "4.5.1",
- "oh-my-opencode-windows-x64": "4.5.1",
- "oh-my-opencode-windows-x64-baseline": "4.5.1",
+ "oh-my-opencode-darwin-arm64": "4.5.12",
+ "oh-my-opencode-darwin-x64": "4.5.12",
+ "oh-my-opencode-darwin-x64-baseline": "4.5.12",
+ "oh-my-opencode-linux-arm64": "4.5.12",
+ "oh-my-opencode-linux-arm64-musl": "4.5.12",
+ "oh-my-opencode-linux-x64": "4.5.12",
+ "oh-my-opencode-linux-x64-baseline": "4.5.12",
+ "oh-my-opencode-linux-x64-musl": "4.5.12",
+ "oh-my-opencode-linux-x64-musl-baseline": "4.5.12",
+ "oh-my-opencode-windows-x64": "4.5.12",
+ "oh-my-opencode-windows-x64-baseline": "4.5.12",
},
"peerDependencies": {
"zod": "^4.0.0",
@@ -431,27 +431,27 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
- "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.5.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-eBpVUGaj4f8CrpETV3j5Uw184QUfSzr8skhTeCemygH8THnbbEQC5lj3KfpeDFD+iWyvG6dKXFgfD80dbFn/qg=="],
+ "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.5.12", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Ivvx5oy+5HFTVWpSKcACUaByWsH6lAf8ICZ55isUmtjARVNZbkEv5c0uXelJfvqBJ114wtjAv333LWc4X30brQ=="],
- "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.5.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/78kDxNiK4UxyNqm0sUWUvBkjlqT1b/XQ7acsR76wWxvcT/rMHOcYhbJCC9tmlfGsgiRj9mrUQQ4GyZ4AUflGQ=="],
+ "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.5.12", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-2LuglSUqPfvGTYU3CgdpeNqBYxVvndpRFiLAFkiSMDWFZ17Yv6tcqEz/PsimwITv5zXug+i9RzgIS1NKVqaESw=="],
- "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.5.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-H40//7oWAAE4/dos5bdaLvu1dZX22DIX8u8KfJnY7/bN/+bYO5WSXu7ANakEebkzVBBHqsMfK5G6GgdvEEcolg=="],
+ "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.5.12", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Zk+ikM/kL3WqT5bTajyLlzlJr1glvwTlt1ETqDmgG6aZ623scdclq5g6ZdgzHl9Bz03BIFEvV00rfnRiWjQf6Q=="],
- "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.5.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-kY2z28+FXEzanYbAJNp6FuxLpr7A1nHywZMU8mQBeGT7evySHojBeAUcrCKAj+nswZkecebwTI+4Q+EfWCKwvQ=="],
+ "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.5.12", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-u73EaDVwPgrbP86zXXs8MvHiP8Wlf/W+utq95uRAu5i1GNPT/TVWXQmhz9muwTYagLgHpsiKYkU77mTsbpIMIQ=="],
- "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.5.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-xHjRCECGzE4ZxlalBGAmptOal8+zY26RBcY0KSK81tZRs0NElEq4Oj+3tsWZXLHrdMeZJ+s2Z04//VpaQrgePA=="],
+ "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.5.12", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-qSLIqmjhc1LnV/zmX1+UZvZZdPcX2aD6cKaoYeNvt0Z7mf5iRzdymnHsABiShO62dztacXniu6KFLm5HdvYECg=="],
- "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-yNBVPT/v/QaAffmEOy8r4jyih0ebGC3E3pD3aZ7YSGJ6c4xbZCMCiSftCDE0XyDT7wSr/0HUzFOVvn+EfLkbzQ=="],
+ "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.5.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-K5wHbomCwOAvMKx6BCpwIt92AkujhpSeXXp871ZsBxExRu4PC00X8lTCaL7wVjM/2sq8apT9c1z8sSK0mrat3Q=="],
- "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/zUu9Lwl3pj9LgP5v1AKsJeOio3ikSaI7WUCAGcnomuGmG0Lbn5RzaWVGxf6kVMOneBzAyQ3sKUde630TI4fzA=="],
+ "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.5.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-byCOE84Z/nztKl8XWJoP4kgIlEUwz0ZPAit00FBSUTart8u326HZduL1Z8mzG0QTKF5oMBYVvaZRWNoPvOd3Tg=="],
- "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-MxyxOZENSouJLinvNFK06eSRNIG4dq5Nh3Zct+dI48aveS/kn7IIDvUTlx5mgCFvGhMygtq/UYgT7n29GKAmpw=="],
+ "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.5.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-mVNLDHcNoQ0gFIuD/MseVrWo2ADB9TmGrzN26bQoHhtqDLdak9s0ukvfcXGICPIMOov3R117UpQ14mpggdjGVQ=="],
- "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-g0gZUb9RAil2LH6QddDvhhEJGBa8w3NzFDBnfEJKRWnZwIs5Z0T4nfDtxvEDCUHXZ5q25DX/jdwTzgggIXiqlw=="],
+ "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.5.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ODhkALqNEd66ndZARYZQRH1psbFWA8oIjskR+NSWWOpJAlcecArnLfJF78M7d0MrUvan8GnxHayHZ/XSrYodUQ=="],
- "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.5.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-JSQduUCWqHac9Sh78pqEPA3K7NCJt0TX4klUOOQbUKlbw1RIjKCh2i9zy7iW5oUGyH7vxXWWvaACSEUIaDD8HA=="],
+ "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.5.12", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mRsNfyyDqvMpc6jD1d2VfNJW4joaeIfBKOcpJ0FtDQkPf7pWc2HVCSi/w/m7y+1dwzOFHFYAQ1UeLpsONbnNgA=="],
- "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.5.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-0u6GeWC9wUeC90dhyHw5W4DSlhAey6P4GRmWcNjnR0nStGnXlca3TOgpJHKEBZdt8tS2tosk9OfGeTZ+la+vZQ=="],
+ "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.5.12", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-15z4hJpzSoPkdOajd3dftso+tpGaJch8v942kttisdZIlWhSp2P0YA1Grs1n9CjsqcCj6JV50KX2eketdguTwQ=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index cc827a0cf..7c4f31ebf 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -530,7 +530,7 @@ Disable built-in hooks via `disabled_hooks`:
{ "disabled_hooks": ["comment-checker"] }
```
-Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `tool-output-truncator`, `question-label-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `model-fallback`, `anthropic-context-window-limit-recovery`, `preemptive-compaction`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `thinking-block-validator`, `tool-pair-validator`, `ralph-loop`, `category-skill-reminder`, `compaction-context-injector`, `compaction-todo-preserver`, `claude-code-hooks`, `auto-slash-command`, `edit-error-recovery`, `json-error-recovery`, `delegate-task-retry`, `prometheus-md-only`, `sisyphus-junior-notepad`, `team-tool-gating`, `no-sisyphus-gpt`, `no-hephaestus-non-gpt`, `start-work`, `atlas`, `unstable-agent-babysitter`, `task-resume-info`, `stop-continuation-guard`, `tasks-todowrite-disabler`, `runtime-fallback`, `write-existing-file-guard`, `bash-file-read-guard`, `anthropic-effort`, `hashline-read-enhancer`, `read-image-resizer`, `todo-description-override`, `webfetch-redirect-guard`, `fsync-skip-warning`, `legacy-plugin-toast`
+Available hooks: `todo-continuation-enforcer`, `session-recovery`, `session-notification`, `comment-checker`, `tool-output-truncator`, `question-label-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `model-fallback`, `anthropic-context-window-limit-recovery`, `preemptive-compaction`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `thinking-block-validator`, `tool-pair-validator`, `ralph-loop`, `category-skill-reminder`, `compaction-context-injector`, `compaction-todo-preserver`, `claude-code-hooks`, `auto-slash-command`, `edit-error-recovery`, `json-error-recovery`, `delegate-task-retry`, `prometheus-md-only`, `sisyphus-junior-notepad`, `team-tool-gating`, `no-sisyphus-gpt`, `no-hephaestus-non-gpt`, `start-work`, `atlas`, `unstable-agent-babysitter`, `task-resume-info`, `stop-continuation-guard`, `tasks-todowrite-disabler`, `runtime-fallback`, `write-existing-file-guard`, `bash-file-read-guard`, `anthropic-effort`, `hashline-read-enhancer`, `read-image-resizer`, `todo-description-override`, `webfetch-redirect-guard`, `fsync-skip-warning`, `legacy-plugin-toast`
Guard hooks such as `team-tool-gating`, `write-existing-file-guard`, `bash-file-read-guard`, `webfetch-redirect-guard`, `prometheus-md-only`, `rules-injector`, `tool-pair-validator`, and `thinking-block-validator` protect safety, permissions, or provider protocol correctness. Disable them only for audited local debugging in a trusted environment.
diff --git a/docs/reference/features.md b/docs/reference/features.md
index 3a8abce12..766e3ea9b 100644
--- a/docs/reference/features.md
+++ b/docs/reference/features.md
@@ -785,7 +785,6 @@ Current composition counts:
| **directory-readme-injector** | PreToolUse + PostToolUse | Auto-injects README.md for directory context. |
| **rules-injector** | PreToolUse + PostToolUse | Injects rules from `.claude/rules/` when conditions match. Supports globs and alwaysApply. |
| **compaction-context-injector** | Event | Preserves critical context during session compaction. |
-| **context-window-monitor** | Event | Monitors context window usage and tracks token consumption. |
| **preemptive-compaction** | Event | Proactively compacts sessions before hitting token limits. |
#### Productivity & Control
diff --git a/src/AGENTS.md b/src/AGENTS.md
index 1232c1bc7..6c70ed846 100644
--- a/src/AGENTS.md
+++ b/src/AGENTS.md
@@ -55,7 +55,7 @@ Counts verified from each composer's return object. Numbers in brackets show cou
```
createHooks()
├─→ createCoreHooks()
- │ ├─ createSessionHooks() # 24: contextWindowMonitor, preemptiveCompaction, sessionRecovery,
+ │ ├─ createSessionHooks() # 23: preemptiveCompaction, sessionRecovery,
│ │ sessionNotification, thinkMode, modelFallback,
│ │ anthropicContextWindowLimitRecovery, autoUpdateChecker,
│ │ agentUsageReminder, nonInteractiveEnv, interactiveBashSession,
diff --git a/src/config/AGENTS.md b/src/config/AGENTS.md
index 276a99ca3..fb096bc7f 100644
--- a/src/config/AGENTS.md
+++ b/src/config/AGENTS.md
@@ -15,7 +15,7 @@ config/schema/
├── agent-overrides.ts # AgentOverrideConfigSchema (21 fields per agent)
├── agent-definitions.ts # custom agent definition schema
├── categories.ts # 8 built-in + custom categories
-├── hooks.ts # HookNameSchema (53 enum values; `team-tool-gating` is the only team-* one in schema — others are wired by direct config gates)
+├── hooks.ts # HookNameSchema (52 enum values; `team-tool-gating` is the only team-* one in schema — others are wired by direct config gates)
├── skills.ts # SkillsConfigSchema (sources, paths, recursive)
├── commands.ts # BuiltinCommandNameSchema
├── experimental.ts # Feature flags incl plugin_load_timeout_ms (min 1000), task_system, max_tools
diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts
index d36e277d9..00c7beefd 100644
--- a/src/config/schema.test.ts
+++ b/src/config/schema.test.ts
@@ -469,6 +469,17 @@ describe("HookNameSchema", () => {
//#then
expect(result.success).toBe(false)
})
+
+ test("rejects removed context-window-monitor hook name", () => {
+ //#given
+ const input = "context-window-monitor"
+
+ //#when
+ const result = HookNameSchema.safeParse(input)
+
+ //#then
+ expect(result.success).toBe(false)
+ })
})
describe("Sisyphus-Junior agent override", () => {
diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts
index 72a43722a..42ad1776e 100644
--- a/src/config/schema/hooks.ts
+++ b/src/config/schema/hooks.ts
@@ -2,7 +2,6 @@ import { z } from "zod"
export const HookNameSchema = z.enum([
"todo-continuation-enforcer",
- "context-window-monitor",
"session-recovery",
"session-notification",
"comment-checker",
diff --git a/src/hooks/AGENTS.md b/src/hooks/AGENTS.md
index 0828e3803..5a51f0af6 100644
--- a/src/hooks/AGENTS.md
+++ b/src/hooks/AGENTS.md
@@ -4,7 +4,7 @@
## OVERVIEW
-52 registered hooks. The 57 directories break down as: 48 registered hook dirs (with `index.ts`) + 6 standalone hook `.ts` files (bash-file-read-guard, context-window-monitor, empty-task-response-detector, preemptive-compaction, session-notification, tool-output-truncator) + support dirs (`shared/`, `team-session-events/`, 5 `zauc-mocks-*`/`zauc-sync-mocks`, `.sisyphus/` legacy state). 5-tier composition wired in `src/plugin/hooks/`. All hooks follow `createXXXHook(deps) → HookFunction` factory pattern.
+51 registered hooks. The 56 directories break down as: 48 registered hook dirs (with `index.ts`) + 5 standalone hook `.ts` files (bash-file-read-guard, empty-task-response-detector, preemptive-compaction, session-notification, tool-output-truncator) + support dirs (`shared/`, `team-session-events/`, 5 `zauc-mocks-*`/`zauc-sync-mocks`, `.sisyphus/` legacy state). 5-tier composition wired in `src/plugin/hooks/`. All hooks follow `createXXXHook(deps) -> HookFunction` factory pattern.
**Unwired WIP (do not modify casually):** `task-reminder/` (has `index.ts` + `createTaskReminderHook` but NOT exported from barrel, NOT imported by any composer) and `hashline-edit-diff-enhancer/` (has only `hook.ts`, NOT registered). Treat as orphaned until wired in.
@@ -12,22 +12,21 @@
| Tier | Composer | Base | With team-mode | Where |
|------|----------|------|----------------|-------|
-| **Session** | `create-session-hooks.ts` | 24 | 24 | OpenCode session lifecycle + chat.params + chat.message |
+| **Session** | `create-session-hooks.ts` | 23 | 23 | OpenCode session lifecycle + chat.params + chat.message |
| **Tool Guard** | `create-tool-guard-hooks.ts` | 16 | 17 | Pre/post tool execution (+1: `team-tool-gating`) |
| **Transform** | `create-transform-hooks.ts` | 5 | 7 | `experimental.chat.messages.transform` (+2: `team-mode-status-injector`, `team-mailbox-injector`) |
| **Continuation** | `create-continuation-hooks.ts` | 7 | 7 | Boulder/atlas/compaction/notification |
| **Skill** | `create-skill-hooks.ts` | 2 | 2 | Skill awareness (categorySkillReminder, autoSlashCommand) |
| **Direct event handlers** | `src/plugin/event.ts` | 0 | +4 | `team-session-events/` sub-files: `team-idle-wake-hint`, `team-lead-orphan-handler`, `team-member-error-handler`, `team-member-status-handler` |
-Total exposed hooks: **54 base, 61 with team-mode** (counts the 4 team-session-events handlers individually).
+Total exposed hooks: **53 base, 60 with team-mode** (counts the 4 team-session-events handlers individually).
Hook name allowlist for `disabled_hooks`: all configurable hook names enumerated in [`src/config/schema/hooks.ts`](file:///Users/yeongyu/local-workspaces/omo/src/config/schema/hooks.ts) `HookNameSchema`. Team-session-event sub-hooks are not individually listed in the schema — they activate together with `team_mode.enabled`.
-### Tier 1: Session Hooks (24)
+### Tier 1: Session Hooks (23)
| Hook | Event | Purpose |
|------|-------|---------|
-| `contextWindowMonitor` | session.idle | Track context usage |
| `preemptiveCompaction` | session.idle | Trigger compaction before limit |
| `sessionRecovery` | session.error | Recover from structural errors (tool_result_missing, thinking_block_order) |
| `sessionNotification` | session.idle | OS notifications on completion |
diff --git a/src/hooks/context-window-monitor.model-context-limits.test.ts b/src/hooks/context-window-monitor.model-context-limits.test.ts
deleted file mode 100644
index 57fc524ab..000000000
--- a/src/hooks/context-window-monitor.model-context-limits.test.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-///
-
-import { describe, expect, it } from "bun:test"
-import { createContextWindowMonitorHook } from "./context-window-monitor"
-
-function createOutput() {
- return { title: "", output: "original", metadata: null }
-}
-
-describe("context-window-monitor modelContextLimitsCache", () => {
- it("does not append reminder below cached non-anthropic threshold", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("opencode/kimi-k2.5-free", 262144)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: false,
- modelContextLimitsCache,
- })
- const sessionID = "ses_non_anthropic_below_threshold"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "opencode",
- modelID: "kimi-k2.5-free",
- finish: true,
- tokens: {
- input: 150000,
- output: 0,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- // then
- expect(output.output).toBe("original")
- })
-
- it("appends reminder above cached non-anthropic threshold", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("opencode/kimi-k2.5-free", 262144)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: false,
- modelContextLimitsCache,
- })
- const sessionID = "ses_non_anthropic_above_threshold"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "opencode",
- modelID: "kimi-k2.5-free",
- finish: true,
- tokens: {
- input: 180000,
- output: 0,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- // then
- expect(output.output).toContain("context remaining")
- expect(output.output).toContain("262,144-token context window")
- expect(output.output).toContain("[Context Status: 72.5% used (190,000/262,144 tokens), 27.5% remaining]")
- expect(output.output).not.toContain("1,000,000")
- })
-
- describe("#given Anthropic provider with cached context limit and 1M mode enabled", () => {
- describe("#when cached usage would exceed 200K but stay below 1M", () => {
- it("#then should ignore the cached limit and skip the reminder", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("anthropic/claude-sonnet-4-5", 200000)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: true,
- modelContextLimitsCache,
- })
- const sessionID = "ses_anthropic_1m_overrides_cached_limit"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- modelID: "claude-sonnet-4-5",
- finish: true,
- tokens: {
- input: 300000,
- output: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- // then
- expect(output.output).toBe("original")
- })
- })
- })
-
- describe("#given Anthropic 4.6 provider with cached context limit and 1M mode disabled", () => {
- describe("#when cached usage is below threshold of cached limit", () => {
- it("#then should respect the cached limit and skip the reminder", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("anthropic/claude-sonnet-4-6", 500000)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: false,
- modelContextLimitsCache,
- })
- const sessionID = "ses_anthropic_cached_limit_respected"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- modelID: "claude-sonnet-4-6",
- finish: true,
- tokens: {
- input: 150000,
- output: 0,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- // then - 160K/500K = 32%, well below 70% threshold
- expect(output.output).toBe("original")
- })
- })
-
- describe("#when cached usage exceeds threshold of cached limit", () => {
- it("#then should use the cached limit for the reminder", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("anthropic/claude-sonnet-4-6", 500000)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: false,
- modelContextLimitsCache,
- })
- const sessionID = "ses_anthropic_cached_limit_exceeded"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- modelID: "claude-sonnet-4-6",
- finish: true,
- tokens: {
- input: 350000,
- output: 0,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- expect(output.output).toContain("context remaining")
- expect(output.output).toContain("500,000-token context window")
- })
- })
- })
-
- describe("#given older Anthropic provider with cached context limit and 1M mode disabled", () => {
- describe("#when cached usage would only exceed the incorrect cached limit", () => {
- it("#then should ignore the cached limit and use the 200K default", async () => {
- // given
- const modelContextLimitsCache = new Map()
- modelContextLimitsCache.set("anthropic/claude-sonnet-4-5", 500000)
-
- const hook = createContextWindowMonitorHook({} as never, {
- anthropicContext1MEnabled: false,
- modelContextLimitsCache,
- })
- const sessionID = "ses_anthropic_older_model_ignores_cached_limit"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- modelID: "claude-sonnet-4-5",
- finish: true,
- tokens: {
- input: 150000,
- output: 0,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- // when
- const output = createOutput()
- await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
-
- // then
- expect(output.output).toContain("context remaining")
- expect(output.output).toContain("200,000-token context window")
- })
- })
- })
-})
diff --git a/src/hooks/context-window-monitor.test.ts b/src/hooks/context-window-monitor.test.ts
deleted file mode 100644
index b664717a6..000000000
--- a/src/hooks/context-window-monitor.test.ts
+++ /dev/null
@@ -1,423 +0,0 @@
-///
-
-import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"
-import { createContextWindowMonitorHook } from "./context-window-monitor"
-
-const ANTHROPIC_CONTEXT_ENV_KEY = "ANTHROPIC_1M_CONTEXT"
-const VERTEX_CONTEXT_ENV_KEY = "VERTEX_ANTHROPIC_1M_CONTEXT"
-
-const originalAnthropicContextEnv = process.env[ANTHROPIC_CONTEXT_ENV_KEY]
-const originalVertexContextEnv = process.env[VERTEX_CONTEXT_ENV_KEY]
-
-function resetContextLimitEnv(): void {
- if (originalAnthropicContextEnv === undefined) {
- delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
- } else {
- process.env[ANTHROPIC_CONTEXT_ENV_KEY] = originalAnthropicContextEnv
- }
-
- if (originalVertexContextEnv === undefined) {
- delete process.env[VERTEX_CONTEXT_ENV_KEY]
- } else {
- process.env[VERTEX_CONTEXT_ENV_KEY] = originalVertexContextEnv
- }
-}
-
-function createMockCtx() {
- return {
- client: {
- session: {
- messages: mock(() => Promise.resolve({ data: [] })),
- },
- },
- directory: "/tmp/test",
- }
-}
-
-describe("context-window-monitor", () => {
- let ctx: ReturnType
-
- beforeEach(() => {
- ctx = createMockCtx()
- delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
- delete process.env[VERTEX_CONTEXT_ENV_KEY]
- })
-
- afterEach(() => {
- resetContextLimitEnv()
- })
-
- // #given event caches token info from message.updated
- // #when tool.execute.after is called
- // #then session.messages() should NOT be called
- it("should use cached token info instead of fetching session.messages()", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_test1"
-
- // Simulate message.updated event with token info
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: {
- input: 50000,
- output: 1000,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- const output = { title: "", output: "test output", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- // session.messages() should NOT have been called
- expect(ctx.client.session.messages).not.toHaveBeenCalled()
- })
-
- // #given no cached token info exists
- // #when tool.execute.after is called
- // #then should skip gracefully without fetching
- it("should skip gracefully when no cached token info exists", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_no_cache"
-
- const output = { title: "", output: "test output", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- // No fetch, no crash
- expect(ctx.client.session.messages).not.toHaveBeenCalled()
- expect(output.output).toBe("test output")
- })
-
- // #given token usage exceeds 70% threshold
- // #when tool.execute.after is called
- // #then context reminder should be appended to output
- it("should append context reminder with actual token counts when usage exceeds threshold", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_high_usage"
-
- // 150K input + 10K cache read = 160K, which is 80% of 200K limit
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: {
- input: 150000,
- output: 1000,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- expect(output.output).toContain("context remaining")
- expect(output.output).toContain("200,000-token context window")
- expect(output.output).toContain("[Context Status: 80.0% used (160,000/200,000 tokens), 20.0% remaining]")
- expect(ctx.client.session.messages).not.toHaveBeenCalled()
- })
-
- // #given total input tokens exceed the resolved actualLimit (e.g. 1M-context
- // Anthropic model where resolveActualContextLimit falls back to the
- // 200K default for the model family)
- // #when tool.execute.after appends the context status block
- // #then the displayed used% must be clamped to 100 and remaining% must not go
- // negative. Safety-tuned models flag the >100% / negative-remaining
- // block as prompt injection (issue #3655).
- it("should clamp displayed percentages when input exceeds actualLimit (regression #3655)", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_overflow"
-
- // 289,370 input + 0 cache against a 200K resolved limit -> 144.7% raw,
- // -44.7% remaining if not clamped.
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: {
- input: 289370,
- output: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- },
- },
- },
- })
-
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- // The block must still be emitted (we are above the 70% threshold).
- expect(output.output).toContain("[Context Status:")
-
- // Extract the displayed percentages and assert clamping.
- const match = output.output.match(
- /\[Context Status: ([\d.-]+)% used \([\d,]+\/[\d,]+ tokens\), ([\d.-]+)% remaining\]/,
- )
- expect(match).not.toBeNull()
- const usedPct = Number(match![1])
- const remainingPct = Number(match![2])
-
- expect(usedPct).toBeLessThanOrEqual(100)
- expect(usedPct).toBeGreaterThanOrEqual(0)
- expect(remainingPct).toBeGreaterThanOrEqual(0)
- expect(remainingPct).toBeLessThanOrEqual(100)
- })
-
- it("should append context reminder for google-vertex-anthropic provider", async () => {
- //#given cached usage for google-vertex-anthropic above threshold
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_vertex_anthropic_high_usage"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "google-vertex-anthropic",
- finish: true,
- tokens: {
- input: 150000,
- output: 1000,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- //#when tool.execute.after runs
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- //#then context reminder should be appended
- expect(output.output).toContain("context remaining")
- })
-
- // #given only a compaction agent summary message update is seen
- // #when tool.execute.after checks context usage
- // #then stale pre-compaction tokens should not create a context reminder
- it("should ignore compaction-agent message updates when caching context usage", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_compaction_agent_context"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- agent: "compaction",
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- modelID: "claude-sonnet-4-6",
- finish: true,
- tokens: {
- input: 150000,
- output: 1000,
- reasoning: 0,
- cache: { read: 10000, write: 0 },
- },
- },
- },
- },
- })
-
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- expect(output.output).toBe("original")
- expect(ctx.client.session.messages).not.toHaveBeenCalled()
- })
-
- // #given session is deleted
- // #when session.deleted event fires
- // #then cached data should be cleaned up
- it("should clean up cache on session.deleted", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_deleted"
-
- // Cache some data
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: { input: 150000, output: 0, reasoning: 0, cache: { read: 10000, write: 0 } },
- },
- },
- },
- })
-
- // Delete session
- await hook.event({
- event: {
- type: "session.deleted",
- properties: { info: { id: sessionID } },
- },
- })
-
- // After deletion, no reminder should fire (cache gone, reminded set gone)
- const output = { title: "", output: "test", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
- expect(output.output).toBe("test")
- })
-
- // #given non-anthropic provider
- // #when message.updated fires
- // #then should not trigger reminder
- it("should ignore non-anthropic providers", async () => {
- const hook = createContextWindowMonitorHook(ctx as never)
- const sessionID = "ses_openai"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "openai",
- finish: true,
- tokens: { input: 200000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- },
- },
- },
- })
-
- const output = { title: "", output: "test", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
- expect(output.output).toBe("test")
- })
-
- it("should use 1M limit when model cache flag is enabled", async () => {
- //#given
- const hook = createContextWindowMonitorHook(ctx as never, {
- anthropicContext1MEnabled: true,
- })
- const sessionID = "ses_1m_flag"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: {
- input: 300000,
- output: 1000,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- },
- },
- },
- })
-
- //#when
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- //#then
- expect(output.output).toBe("original")
- })
-
- it("should keep env var fallback when model cache flag is disabled", async () => {
- //#given
- process.env[ANTHROPIC_CONTEXT_ENV_KEY] = "true"
- const hook = createContextWindowMonitorHook(ctx as never, {
- anthropicContext1MEnabled: false,
- })
- const sessionID = "ses_env_fallback"
-
- await hook.event({
- event: {
- type: "message.updated",
- properties: {
- info: {
- role: "assistant",
- sessionID,
- providerID: "anthropic",
- finish: true,
- tokens: {
- input: 300000,
- output: 1000,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- },
- },
- },
- })
-
- //#when
- const output = { title: "", output: "original", metadata: null }
- await hook["tool.execute.after"](
- { tool: "bash", sessionID, callID: "call_1" },
- output
- )
-
- //#then
- expect(output.output).toBe("original")
- })
-})
diff --git a/src/hooks/context-window-monitor.ts b/src/hooks/context-window-monitor.ts
deleted file mode 100644
index 507ea10b7..000000000
--- a/src/hooks/context-window-monitor.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import type { PluginInput } from "@opencode-ai/plugin"
-import {
- resolveActualContextLimit,
- type ContextLimitModelCacheState,
-} from "../shared/context-limit-resolver"
-import { isCompactionAgent } from "../shared/compaction-marker"
-import { resolveMessageEventSessionID, resolveSessionEventID } from "../shared/event-session-id"
-import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive"
-
-const CONTEXT_WARNING_THRESHOLD = 0.70
-
-function createContextReminder(actualLimit: number): string {
- const limitTokens = actualLimit.toLocaleString()
-
- return `${createSystemDirective(SystemDirectiveTypes.CONTEXT_WINDOW_MONITOR)}
-
-You are using a ${limitTokens}-token context window.
-You still have context remaining - do NOT rush or skip tasks.
-Complete your work thoroughly and methodically.`
-}
-
-interface TokenInfo {
- input: number
- output: number
- reasoning: number
- cache: { read: number; write: number }
-}
-
-interface CachedTokenState {
- providerID: string
- modelID: string
- tokens: TokenInfo
-}
-
-export function createContextWindowMonitorHook(
- _ctx: PluginInput,
- modelCacheState?: ContextLimitModelCacheState,
-) {
- const remindedSessions = new Set()
- const tokenCache = new Map()
-
- const toolExecuteAfter = async (
- input: { tool: string; sessionID: string; callID: string },
- output: { title: string; output: string; metadata: unknown }
- ) => {
- const { sessionID } = input
-
- if (remindedSessions.has(sessionID)) return
-
- const cached = tokenCache.get(sessionID)
- if (!cached) return
-
- const actualLimit = resolveActualContextLimit(
- cached.providerID,
- cached.modelID,
- modelCacheState,
- )
-
- if (!actualLimit) return
-
- const lastTokens = cached.tokens
- const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
-
- const actualUsagePercentage = totalInputTokens / actualLimit
-
- if (actualUsagePercentage < CONTEXT_WARNING_THRESHOLD) return
-
- remindedSessions.add(sessionID)
-
- // Clamp the displayed percentages so the block stays trustworthy when the
- // resolved actualLimit underestimates the model's real context window
- // (e.g. a 1M-context Anthropic model that falls back to the 200K default).
- // Without clamping, the block would advertise >100% used and a negative
- // "remaining" - safety-tuned models flag exactly that pattern as a prompt
- // injection and refuse to follow the directive (issue #3655).
- const clampedPercentage = Math.min(Math.max(actualUsagePercentage, 0), 1)
- const usedPct = (clampedPercentage * 100).toFixed(1)
- const remainingPct = ((1 - clampedPercentage) * 100).toFixed(1)
- const usedTokens = totalInputTokens.toLocaleString()
- const limitTokens = actualLimit.toLocaleString()
-
- output.output += `\n\n${createContextReminder(actualLimit)}
-[Context Status: ${usedPct}% used (${usedTokens}/${limitTokens} tokens), ${remainingPct}% remaining]`
- }
-
- const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
- const props = event.properties as Record | undefined
-
- if (event.type === "session.deleted") {
- const sessionID = resolveSessionEventID(props)
- if (sessionID) {
- remindedSessions.delete(sessionID)
- tokenCache.delete(sessionID)
- }
- }
-
- if (event.type === "message.updated") {
- const info = props?.info as {
- agent?: unknown
- role?: string
- sessionID?: string
- providerID?: string
- modelID?: string
- finish?: unknown
- tokens?: TokenInfo
- } | undefined
-
- const finish = info?.finish
- if (!info || info.role !== "assistant" || !finish) return
- if (isCompactionAgent(info.agent)) return
- const sessionID = resolveMessageEventSessionID(props)
- if (!sessionID || !info.providerID || !info.tokens) return
-
- tokenCache.set(sessionID, {
- providerID: info.providerID,
- modelID: info.modelID ?? "",
- tokens: info.tokens,
- })
- }
- }
-
- return {
- "tool.execute.after": toolExecuteAfter,
- event: eventHandler,
- }
-}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 493414d34..4b446f5c5 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1,5 +1,4 @@
export { createTodoContinuationEnforcer, type TodoContinuationEnforcer } from "./todo-continuation-enforcer";
-export { createContextWindowMonitorHook } from "./context-window-monitor";
export { createSessionNotification } from "./session-notification";
export { sendSessionNotification, playSessionNotificationSound, detectPlatform, getDefaultSoundPath } from "./session-notification-sender";
export { buildWindowsToastScript, escapeAppleScriptText, escapePowerShellSingleQuotedText } from "./session-notification-formatting";
diff --git a/src/plugin/event-compaction-agent.test.ts b/src/plugin/event-compaction-agent.test.ts
index b5888d7fe..0a52b5322 100644
--- a/src/plugin/event-compaction-agent.test.ts
+++ b/src/plugin/event-compaction-agent.test.ts
@@ -29,7 +29,6 @@ function createMinimalEventHandler() {
sessionNotification: async () => {},
todoContinuationEnforcer: { handler: async () => {} },
unstableAgentBabysitter: { event: async () => {} },
- contextWindowMonitor: { event: async () => {} },
directoryAgentsInjector: { event: async () => {} },
directoryReadmeInjector: { event: async () => {} },
rulesInjector: { event: async () => {} },
diff --git a/src/plugin/event.test.ts b/src/plugin/event.test.ts
index df3eb27d9..9603fb445 100644
--- a/src/plugin/event.test.ts
+++ b/src/plugin/event.test.ts
@@ -391,7 +391,7 @@ describe("createEventHandler - idle deduplication", () => {
expect(spawnTmuxPane).toHaveBeenCalledTimes(1)
})
- it("does NOT dedup real-idle-after-synthetic-idle within 500ms", async () => {
+ it("#given session.status already emitted a synthetic idle #when real session.idle follows immediately #then hooks run once", async () => {
//#given
const dispatchCalls: EventInput[] = []
const eventHandler = createIdleTrackingEventHandler(dispatchCalls)
@@ -415,11 +415,9 @@ describe("createEventHandler - idle deduplication", () => {
}))
//#then
- expect(dispatchCalls).toHaveLength(2)
+ expect(dispatchCalls).toHaveLength(1)
expect(dispatchCalls[0]?.event.type).toBe("session.idle")
expect((dispatchCalls[0]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
- expect(dispatchCalls[1]?.event.type).toBe("session.idle")
- expect((dispatchCalls[1]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
})
it("#given idle recovery handles an interrupted tool turn #when session.idle arrives #then later idle hooks are skipped for that event", async () => {
@@ -619,7 +617,7 @@ describe("createEventHandler - idle deduplication", () => {
}
})
- it("keeps other session dedup state untouched when bypassing synthetic-idle for current session", async () => {
+ it("keeps other session dedup state untouched when suppressing real-idle-after-synthetic-idle", async () => {
//#given
const originalDateNow = Date.now
let currentNow = 30_000
@@ -679,7 +677,7 @@ describe("createEventHandler - idle deduplication", () => {
}))
//#then
- expect(dispatchedSessionIds).toEqual(["ses_a", "ses_b", "ses_a"])
+ expect(dispatchedSessionIds).toEqual(["ses_a", "ses_b"])
} finally {
Date.now = originalDateNow
}
@@ -828,7 +826,6 @@ describe("createEventHandler - idle deduplication", () => {
sessionNotification: async () => {},
todoContinuationEnforcer: { handler: async () => {} },
unstableAgentBabysitter: { event: async () => {} },
- contextWindowMonitor: { event: async () => {} },
directoryAgentsInjector: { event: async () => {} },
directoryReadmeInjector: { event: async () => {} },
rulesInjector: { event: async () => {} },
@@ -913,7 +910,6 @@ describe("createEventHandler - idle deduplication", () => {
sessionNotification: async () => {},
todoContinuationEnforcer: { handler: async () => {} },
unstableAgentBabysitter: { event: async () => {} },
- contextWindowMonitor: { event: async () => {} },
directoryAgentsInjector: { event: async () => {} },
directoryReadmeInjector: { event: async () => {} },
rulesInjector: { event: async () => {} },
@@ -970,7 +966,6 @@ describe("createEventHandler - idle deduplication", () => {
sessionNotification: async () => {},
todoContinuationEnforcer: { handler: async () => {} },
unstableAgentBabysitter: { event: async () => {} },
- contextWindowMonitor: { event: async () => {} },
directoryAgentsInjector: { event: async () => {} },
directoryReadmeInjector: { event: async () => {} },
rulesInjector: { event: async () => {} },
diff --git a/src/plugin/event.ts b/src/plugin/event.ts
index cb653f8d7..2c0d3a4eb 100644
--- a/src/plugin/event.ts
+++ b/src/plugin/event.ts
@@ -310,7 +310,6 @@ export function createEventHandler(args: {
await runEventHookSafely("sessionNotification", hooks.sessionNotification, input);
await runEventHookSafely("todoContinuationEnforcer", hooks.todoContinuationEnforcer?.handler, input);
await runEventHookSafely("unstableAgentBabysitter", hooks.unstableAgentBabysitter?.event, input);
- await runEventHookSafely("contextWindowMonitor", hooks.contextWindowMonitor?.event, input);
await runEventHookSafely("preemptiveCompaction", hooks.preemptiveCompaction?.event, input);
await runEventHookSafely("directoryAgentsInjector", hooks.directoryAgentsInjector?.event, input);
await runEventHookSafely("directoryReadmeInjector", hooks.directoryReadmeInjector?.event, input);
@@ -594,12 +593,6 @@ export function createEventHandler(args: {
const emittedAt = recentSyntheticIdles.get(sessionID);
if (emittedAt !== undefined && now - emittedAt < DEDUP_WINDOW_MS) {
recentSyntheticIdles.delete(sessionID);
- // Let real idle events through even when a synthetic idle fired moments earlier.
- // OpenCode diagnostics expect a concrete session.idle event signal.
- const lastAnyIdleAt = recentAnyIdles.get(sessionID);
- if (lastAnyIdleAt === emittedAt) {
- recentAnyIdles.delete(sessionID);
- }
}
}
const recovered = await recoverInterruptedToolResultsOnIdleEvent(input);
diff --git a/src/plugin/hooks/create-session-hooks.test.ts b/src/plugin/hooks/create-session-hooks.test.ts
index 42a091f4f..d443a5a78 100644
--- a/src/plugin/hooks/create-session-hooks.test.ts
+++ b/src/plugin/hooks/create-session-hooks.test.ts
@@ -55,6 +55,23 @@ describe("createSessionHooks", () => {
expect(result.modelFallback).not.toBeNull()
})
+ it("does not create removed context window monitor hook", () => {
+ // given
+ const pluginConfig = {} as OhMyOpenCodeConfig
+
+ // when
+ const result = createSessionHooks({
+ ctx: mockContext,
+ pluginConfig,
+ modelCacheState: mockModelCacheState,
+ isHookEnabled: (hookName) => hookName === "context-window-monitor",
+ safeHookEnabled: true,
+ })
+
+ // then
+ expect("contextWindowMonitor" in result).toBe(false)
+ })
+
it("skips interactive bash session hook when tmux integration is disabled", () => {
// given
const pluginConfig = {
diff --git a/src/plugin/hooks/create-session-hooks.ts b/src/plugin/hooks/create-session-hooks.ts
index 69208820a..f29e1b23e 100644
--- a/src/plugin/hooks/create-session-hooks.ts
+++ b/src/plugin/hooks/create-session-hooks.ts
@@ -5,7 +5,6 @@ import type { ModelCacheState } from "../../plugin-state"
import type { PluginContext } from "../types"
import {
- createContextWindowMonitorHook,
createSessionRecoveryHook,
createSessionNotification,
createThinkModeHook,
@@ -41,7 +40,6 @@ import { sessionExists } from "../../tools"
import { isTmuxIntegrationEnabled } from "../../create-runtime-tmux-config"
export type SessionHooks = {
- contextWindowMonitor: ReturnType | null
preemptiveCompaction: ReturnType | null
sessionRecovery: ReturnType | null
sessionNotification: ReturnType | null
@@ -80,11 +78,6 @@ export function createSessionHooks(args: {
const safeHook = (hookName: HookName, factory: () => T): T | null =>
safeCreateHook(hookName, factory, { enabled: safeHookEnabled })
- const contextWindowMonitor = isHookEnabled("context-window-monitor")
- ? safeHook("context-window-monitor", () =>
- createContextWindowMonitorHook(ctx, modelCacheState))
- : null
-
const preemptiveCompaction =
isHookEnabled("preemptive-compaction") &&
pluginConfig.experimental?.preemptive_compaction
@@ -277,7 +270,6 @@ export function createSessionHooks(args: {
: null
return {
- contextWindowMonitor,
preemptiveCompaction,
sessionRecovery,
sessionNotification,
diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts
index 75bd1c0ee..9b4617461 100644
--- a/src/plugin/tool-execute-after.ts
+++ b/src/plugin/tool-execute-after.ts
@@ -162,7 +162,6 @@ export function createToolExecuteAfterHandler(args: {
await hooks.toolOutputTruncator?.["tool.execute.after"]?.(hookInput, output)
await hooks.claudeCodeHooks?.["tool.execute.after"]?.(hookInput, output)
await hooks.preemptiveCompaction?.["tool.execute.after"]?.(hookInput, output)
- await hooks.contextWindowMonitor?.["tool.execute.after"]?.(hookInput, output)
await hooks.commentChecker?.["tool.execute.after"]?.(hookInput, output)
await hooks.directoryAgentsInjector?.["tool.execute.after"]?.(hookInput, output)
await hooks.directoryReadmeInjector?.["tool.execute.after"]?.(hookInput, output)
diff --git a/src/tools/call-omo-agent/reused-sync-session-delete-cleanup.test.ts b/src/tools/call-omo-agent/reused-sync-session-delete-cleanup.test.ts
index cfaf9f497..0967ccbad 100644
--- a/src/tools/call-omo-agent/reused-sync-session-delete-cleanup.test.ts
+++ b/src/tools/call-omo-agent/reused-sync-session-delete-cleanup.test.ts
@@ -31,7 +31,6 @@ function createMinimalEventHandler() {
sessionNotification: async () => {},
todoContinuationEnforcer: { handler: async () => {} },
unstableAgentBabysitter: { event: async () => {} },
- contextWindowMonitor: { event: async () => {} },
directoryAgentsInjector: { event: async () => {} },
directoryReadmeInjector: { event: async () => {} },
rulesInjector: { event: async () => {} },