From 65bc742881b8808ec9a0341f23623975494dc1e8 Mon Sep 17 00:00:00 2001 From: Chocothin Date: Sun, 1 Mar 2026 22:49:47 +0900 Subject: [PATCH 1/6] fix(tool-config): respect question permission from OPENCODE_CONFIG_CONTENT applyToolConfig() unconditionally set question permission based only on OPENCODE_CLI_RUN_MODE, ignoring the question:deny already configured via OPENCODE_CONFIG_CONTENT. This caused agents to hang in headless environments (e.g. Maestro Auto Run) where the host sets question:deny but does not know about the plugin-internal OPENCODE_CLI_RUN_MODE variable. Read permission.question from OPENCODE_CONFIG_CONTENT and give it highest priority: config deny > CLI run mode deny > default allow. --- .../tool-config-handler.test.ts | 105 +++++++++++++++++- src/plugin-handlers/tool-config-handler.ts | 17 ++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/plugin-handlers/tool-config-handler.test.ts b/src/plugin-handlers/tool-config-handler.test.ts index 4ba70497a..0ef60d56f 100644 --- a/src/plugin-handlers/tool-config-handler.test.ts +++ b/src/plugin-handlers/tool-config-handler.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "bun:test" +import { describe, it, expect, beforeEach, afterEach } from "bun:test" import { applyToolConfig } from "./tool-config-handler" import type { OhMyOpenCodeConfig } from "../config" @@ -56,6 +56,109 @@ describe("applyToolConfig", () => { }) }) + describe("#given OPENCODE_CONFIG_CONTENT has question set to deny", () => { + let originalConfigContent: string | undefined + let originalCliRunMode: string | undefined + + beforeEach(() => { + originalConfigContent = process.env.OPENCODE_CONFIG_CONTENT + originalCliRunMode = process.env.OPENCODE_CLI_RUN_MODE + }) + + afterEach(() => { + if (originalConfigContent === undefined) { + delete process.env.OPENCODE_CONFIG_CONTENT + } else { + process.env.OPENCODE_CONFIG_CONTENT = originalConfigContent + } + if (originalCliRunMode === undefined) { + delete process.env.OPENCODE_CLI_RUN_MODE + } else { + process.env.OPENCODE_CLI_RUN_MODE = originalCliRunMode + } + }) + + describe("#when config explicitly denies question permission", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s even without CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "deny" }, + }) + delete process.env.OPENCODE_CLI_RUN_MODE + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + + describe("#when config does not deny question permission", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should allow question for %s in interactive mode", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "allow" }, + }) + delete process.env.OPENCODE_CLI_RUN_MODE + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("allow") + }, + ) + }) + + describe("#when CLI_RUN_MODE is true and config does not deny", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s via CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: {}, + }) + process.env.OPENCODE_CLI_RUN_MODE = "true" + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + + describe("#when config deny overrides CLI_RUN_MODE allow", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s when config says deny regardless of CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "deny" }, + }) + process.env.OPENCODE_CLI_RUN_MODE = "false" + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + }) + describe("#given task_system is disabled", () => { describe("#when applying tool config", () => { it.each([ diff --git a/src/plugin-handlers/tool-config-handler.ts b/src/plugin-handlers/tool-config-handler.ts index e488d2da9..1168e272e 100644 --- a/src/plugin-handlers/tool-config-handler.ts +++ b/src/plugin-handlers/tool-config-handler.ts @@ -3,6 +3,17 @@ import { getAgentDisplayName } from "../shared/agent-display-names"; type AgentWithPermission = { permission?: Record }; +function getConfigQuestionPermission(): string | null { + const configContent = process.env.OPENCODE_CONFIG_CONTENT; + if (!configContent) return null; + try { + const parsed = JSON.parse(configContent); + return parsed?.permission?.question ?? null; + } catch { + return null; + } +} + function agentByKey(agentResult: Record, key: string): AgentWithPermission | undefined { return (agentResult[key] ?? agentResult[getAgentDisplayName(key)]) as | AgentWithPermission @@ -32,7 +43,11 @@ export function applyToolConfig(params: { }; const isCliRunMode = process.env.OPENCODE_CLI_RUN_MODE === "true"; - const questionPermission = isCliRunMode ? "deny" : "allow"; + const configQuestionPermission = getConfigQuestionPermission(); + const questionPermission = + configQuestionPermission === "deny" ? "deny" : + isCliRunMode ? "deny" : + "allow"; const librarian = agentByKey(params.agentResult, "librarian"); if (librarian) { From f67b605f7a7a6e3e3d09cf51f3c94ca109298d19 Mon Sep 17 00:00:00 2001 From: SeeYouCowboi Date: Wed, 4 Mar 2026 16:48:33 +0800 Subject: [PATCH 2/6] fix: also invalidate plugin from CACHE_DIR in invalidatePackage Fix #2289 invalidatePackage() only removed the plugin from USER_CONFIG_DIR/node_modules/, but bun may install it in CACHE_DIR/node_modules/ on some systems. This left a stale copy behind, causing the startup toast to keep showing the old version even after the auto-update completed successfully. Now both candidate locations are checked and removed so the reinstalled version is loaded on the next restart. --- src/hooks/auto-update-checker/cache.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/hooks/auto-update-checker/cache.ts b/src/hooks/auto-update-checker/cache.ts index e5df33df7..9cf312a36 100644 --- a/src/hooks/auto-update-checker/cache.ts +++ b/src/hooks/auto-update-checker/cache.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs" import * as path from "node:path" -import { PACKAGE_NAME, USER_CONFIG_DIR } from "./constants" +import { CACHE_DIR, PACKAGE_NAME, USER_CONFIG_DIR } from "./constants" import { log } from "../../shared/logger" interface BunLockfile { @@ -48,17 +48,22 @@ function removeFromBunLock(packageName: string): boolean { export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean { try { - const pkgDir = path.join(USER_CONFIG_DIR, "node_modules", packageName) + const pkgDirs = [ + path.join(USER_CONFIG_DIR, "node_modules", packageName), + path.join(CACHE_DIR, "node_modules", packageName), + ] const pkgJsonPath = path.join(USER_CONFIG_DIR, "package.json") let packageRemoved = false let dependencyRemoved = false let lockRemoved = false - if (fs.existsSync(pkgDir)) { - fs.rmSync(pkgDir, { recursive: true, force: true }) - log(`[auto-update-checker] Package removed: ${pkgDir}`) - packageRemoved = true + for (const pkgDir of pkgDirs) { + if (fs.existsSync(pkgDir)) { + fs.rmSync(pkgDir, { recursive: true, force: true }) + log(`[auto-update-checker] Package removed: ${pkgDir}`) + packageRemoved = true + } } if (fs.existsSync(pkgJsonPath)) { From adaeaca8e93c92e8a682a0c80b28f3942e679c1c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Mar 2026 03:36:52 +0900 Subject: [PATCH 3/6] fix: add NODE_AUTH_TOKEN to publish-main job for npm auth The publish-main job relied on npm trusted publishing (OIDC) which broke after the repo rename from oh-my-opencode to oh-my-openagent. Adding explicit NODE_AUTH_TOKEN restores auth while --provenance still uses OIDC for Sigstore attestation. Fixes #2373 --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8f95aeae4..f22c81ce4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -213,8 +213,8 @@ jobs: fi npm publish --access public --provenance $TAG_ARG env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} NPM_CONFIG_PROVENANCE: true - - name: Publish oh-my-openagent if: steps.check.outputs.skip != 'true' run: | @@ -242,8 +242,8 @@ jobs: fi npm publish --access public --provenance $TAG_ARG || echo "oh-my-openagent publish may have failed (package may already exist)" env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} NPM_CONFIG_PROVENANCE: true - - name: Restore package.json if: steps.check.outputs.skip != 'true' run: | From 4e352f9cafc3ec14f17de2eafc69e21b9e3d45fc Mon Sep 17 00:00:00 2001 From: CrazyRabbit Date: Fri, 20 Feb 2026 21:14:07 +0200 Subject: [PATCH 4/6] fix(session-notification): add grace period to prevent late events from cancelling idle notifications --- bun.lock | 44 ++++++------ src/hooks/session-notification-scheduler.ts | 22 ++++++ src/hooks/session-notification.test.ts | 76 ++++++++++++++++++++- src/hooks/session-notification.ts | 2 + 4 files changed, 121 insertions(+), 23 deletions(-) diff --git a/bun.lock b/bun.lock index e99fefc76..458df7db2 100644 --- a/bun.lock +++ b/bun.lock @@ -29,17 +29,17 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.10.0", - "oh-my-opencode-darwin-x64": "3.10.0", - "oh-my-opencode-darwin-x64-baseline": "3.10.0", - "oh-my-opencode-linux-arm64": "3.10.0", - "oh-my-opencode-linux-arm64-musl": "3.10.0", - "oh-my-opencode-linux-x64": "3.10.0", - "oh-my-opencode-linux-x64-baseline": "3.10.0", - "oh-my-opencode-linux-x64-musl": "3.10.0", - "oh-my-opencode-linux-x64-musl-baseline": "3.10.0", - "oh-my-opencode-windows-x64": "3.10.0", - "oh-my-opencode-windows-x64-baseline": "3.10.0", + "oh-my-opencode-darwin-arm64": "3.11.0", + "oh-my-opencode-darwin-x64": "3.11.0", + "oh-my-opencode-darwin-x64-baseline": "3.11.0", + "oh-my-opencode-linux-arm64": "3.11.0", + "oh-my-opencode-linux-arm64-musl": "3.11.0", + "oh-my-opencode-linux-x64": "3.11.0", + "oh-my-opencode-linux-x64-baseline": "3.11.0", + "oh-my-opencode-linux-x64-musl": "3.11.0", + "oh-my-opencode-linux-x64-musl-baseline": "3.11.0", + "oh-my-opencode-windows-x64": "3.11.0", + "oh-my-opencode-windows-x64-baseline": "3.11.0", }, }, }, @@ -238,27 +238,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.10.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KQ1Nva4eU03WIaQI8BiEgizYJAeddUIaC8dmks0Ug/2EkH6VyNj41+shI58HFGN9Jlg9Fd6MxpOW92S3JUHjOw=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.11.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-TLMCq1HXU1BOp3KWdcITQqT3TQcycAxvdYELMzY/17HUVHjvJiaLjyrbmw0VlgBjoRZOlmsedK+o59y7WRM40Q=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-PydZ6wKyLZzikSZA3Q89zKZwFyg0Ouqd/S6zDsf1zzpUWT1t5EcpBtYFwuscD7L4hdkIEFm8wxnnBkz5i6BEiA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-szKfyAYbI3Mp6rqxHxcHhAE8noxIzBbpfvKX0acyMB/KRqUCtgTe13aic5tz/W/Agp9NU1PVasyqjJjAtE73JA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-yOaVd0E1qspT2xP/BMJaJ/rpFTwkOh9U/SAk6uOuxHld6dZGI9e2Oq8F3pSD16xHnnpaz4VzadtT6HkvPdtBYg=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QZ+2LCcXK6NPopYSxFCHrYAqLccN+jMQ0YrQI+QBlsajLSsnSqfv6W3Vaxv95iLWhGey3v2oGu5OUgdW9fjy9w=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-pLzcPMuzBb1tpVgqMilv7QdsE2xTMLCWT3b807mzjt0302fZTfm6emwymCG25RamHdq7+mI2B0rN7hjvbymFog=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NZMbNG+kJ0FTS4u5xhuBUjJ2K2Tds8sETbdq1VPT52rd+mIbVVSbugfppagEh9wbNqXqJY1HwQ/+4Q+NoGGXhQ=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ca61zr+X8q0ipO2x72qU+4R6Dsr168OM9aXI6xDHbrr0l3XZlRO8xuwQidch1vE5QRv2/IJT10KjAFInCERDug=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-f0GO63uAwzBisotiMneA7Pi2xPXUxvdX5QRC6z4X2xoB8F7/jT+2+dY8J03eM+YJVAwQWR/74hm5HFSenqMeIA=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m0Ys8Vnl8jUNRE5/aIseNOF1H57/W77xh3vkyBVfnjzHwQdEUWZz3IdoHaEWIFgIP2+fsNXRHqpx7Pbtuhxo6Q=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OzIgo26t1EbooHwzmli+4aemO6YqXEhJTBth8L688K1CI/xF567G3+uJemZ9U7NI+miHJRoKHcidNnaAi7bgGQ=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-a6OhfqMXhOTq1On8YHRRlVsNtMx84kgNAnStk/sY1Dw0kXU68QK4tWXVF+wNdiRG3egeM2SvjhJ5RhWlr3CCNQ=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ac7TfBli+gaHVu4aBtP2ADWzetrFZOs+h1K39KsR6MOhDZBl+B6B1S47U+BXGWtUKIRYm4uUo578XdnmsDanoA=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-lZkoEWwmrlVoZKewHNslUmQ2D6eWi1YqsoZMTd3qRj8V4XI6TDZHxg86hw4oxZ/EnKO4un+r83tb09JAAb1nNQ=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OvOsPNuvZQug4tGjbcpbvh67tud1K84A3Qskt9S7BHBIvMH129iV/2GGyr6aca8gwvd5T+X05H/s5mnPG6jkBQ=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UqArUpatMuen8+hZhMSbScaSmJlcwkEtf/IzDN1iYO0CttvhyYMUmm3el/1gWTAcaGNDFNkGmTli5WNYhnm2lA=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-fSsyVAFMoOljD+zqRO6lG3f9ka1YRLMp6rNSsPWkLEKKIyEdw1J0GcmA/48VI1NgtnEgKqS3Ft87tees1woyBw=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BivOu1+Yty9N6VSmNzmxROZqjQKu3ImWjooKZDfczvYLDQmZV104QcOKV6bmdOCpHrqQ7cvdbygmeiJeRoYShg=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-k9F3/9r3pFnUVJW36+zF06znUdUzcnJp+BdvDcaJrcuuM516ECwCH0yY5WbDTFFydFBQBkPBJX9DwU8dmc4kHA=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BBv+dNPuh9LEuqXUJLXNsvi3vL30zS1qcJuzlq/s8rYHry+VvEVXCRcMm5Vo0CVna8bUZf5U8MDkGDHOAiTeEw=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mRRcCHC43TLUuIkDs0ASAUGo3DpMIkSeIPDdtBrh1eJZyVulJRGBoniIk/+Y+RJwtsUoC+lUX/auQelzJsMpbQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/hooks/session-notification-scheduler.ts b/src/hooks/session-notification-scheduler.ts index d28abd112..367ca5dcb 100644 --- a/src/hooks/session-notification-scheduler.ts +++ b/src/hooks/session-notification-scheduler.ts @@ -9,6 +9,8 @@ type SessionNotificationConfig = { idleConfirmationDelay: number skipIfIncompleteTodos: boolean maxTrackedSessions: number + /** Grace period in ms to ignore late-arriving activity events after scheduling (default: 100) */ + activityGracePeriodMs?: number } export function createIdleNotificationScheduler(options: { @@ -24,6 +26,9 @@ export function createIdleNotificationScheduler(options: { const sessionActivitySinceIdle = new Set() const notificationVersions = new Map() const executingNotifications = new Set() + const scheduledAt = new Map() + + const activityGracePeriodMs = options.config.activityGracePeriodMs ?? 100 function cleanupOldSessions(): void { const maxSessions = options.config.maxTrackedSessions @@ -43,6 +48,10 @@ export function createIdleNotificationScheduler(options: { const sessionsToRemove = Array.from(executingNotifications).slice(0, executingNotifications.size - maxSessions) sessionsToRemove.forEach((id) => executingNotifications.delete(id)) } + if (scheduledAt.size > maxSessions) { + const sessionsToRemove = Array.from(scheduledAt.keys()).slice(0, scheduledAt.size - maxSessions) + sessionsToRemove.forEach((id) => scheduledAt.delete(id)) + } } function cancelPendingNotification(sessionID: string): void { @@ -51,11 +60,17 @@ export function createIdleNotificationScheduler(options: { clearTimeout(timer) pendingTimers.delete(sessionID) } + scheduledAt.delete(sessionID) sessionActivitySinceIdle.add(sessionID) notificationVersions.set(sessionID, (notificationVersions.get(sessionID) ?? 0) + 1) } function markSessionActivity(sessionID: string): void { + const scheduledTime = scheduledAt.get(sessionID) + if (scheduledTime && Date.now() - scheduledTime < activityGracePeriodMs) { + return + } + cancelPendingNotification(sessionID) if (!executingNotifications.has(sessionID)) { notifiedSessions.delete(sessionID) @@ -65,22 +80,26 @@ export function createIdleNotificationScheduler(options: { async function executeNotification(sessionID: string, version: number): Promise { if (executingNotifications.has(sessionID)) { pendingTimers.delete(sessionID) + scheduledAt.delete(sessionID) return } if (notificationVersions.get(sessionID) !== version) { pendingTimers.delete(sessionID) + scheduledAt.delete(sessionID) return } if (sessionActivitySinceIdle.has(sessionID)) { sessionActivitySinceIdle.delete(sessionID) pendingTimers.delete(sessionID) + scheduledAt.delete(sessionID) return } if (notifiedSessions.has(sessionID)) { pendingTimers.delete(sessionID) + scheduledAt.delete(sessionID) return } @@ -113,6 +132,7 @@ export function createIdleNotificationScheduler(options: { } finally { executingNotifications.delete(sessionID) pendingTimers.delete(sessionID) + scheduledAt.delete(sessionID) if (sessionActivitySinceIdle.has(sessionID)) { notifiedSessions.delete(sessionID) sessionActivitySinceIdle.delete(sessionID) @@ -126,6 +146,7 @@ export function createIdleNotificationScheduler(options: { if (executingNotifications.has(sessionID)) return sessionActivitySinceIdle.delete(sessionID) + scheduledAt.set(sessionID, Date.now()) const currentVersion = (notificationVersions.get(sessionID) ?? 0) + 1 notificationVersions.set(sessionID, currentVersion) @@ -144,6 +165,7 @@ export function createIdleNotificationScheduler(options: { sessionActivitySinceIdle.delete(sessionID) notificationVersions.delete(sessionID) executingNotifications.delete(sessionID) + scheduledAt.delete(sessionID) } return { diff --git a/src/hooks/session-notification.test.ts b/src/hooks/session-notification.test.ts index 8ab8933ce..d681d1668 100644 --- a/src/hooks/session-notification.test.ts +++ b/src/hooks/session-notification.test.ts @@ -195,8 +195,9 @@ describe("session-notification", () => { setMainSession(mainSessionID) const hook = createSessionNotification(createMockPluginInput(), { - idleConfirmationDelay: 100, // Long delay + idleConfirmationDelay: 100, skipIfIncompleteTodos: false, + activityGracePeriodMs: 0, }) // when - session goes idle @@ -272,6 +273,7 @@ describe("session-notification", () => { const hook = createSessionNotification(createMockPluginInput(), { idleConfirmationDelay: 50, skipIfIncompleteTodos: false, + activityGracePeriodMs: 0, }) // when - session goes idle, then message.updated fires @@ -306,6 +308,7 @@ describe("session-notification", () => { const hook = createSessionNotification(createMockPluginInput(), { idleConfirmationDelay: 50, skipIfIncompleteTodos: false, + activityGracePeriodMs: 0, }) // when - session goes idle, then tool.execute.before fires @@ -509,4 +512,75 @@ describe("session-notification", () => { } } }) + + test("should ignore activity events within grace period", async () => { + // given - main session is set + const mainSessionID = "main-grace" + setMainSession(mainSessionID) + + const hook = createSessionNotification(createMockPluginInput(), { + idleConfirmationDelay: 50, + skipIfIncompleteTodos: false, + activityGracePeriodMs: 100, + }) + + // when - session goes idle + await hook({ + event: { + type: "session.idle", + properties: { sessionID: mainSessionID }, + }, + }) + + // when - activity happens immediately (within grace period) + await hook({ + event: { + type: "tool.execute.before", + properties: { sessionID: mainSessionID }, + }, + }) + + // Wait for idle delay to pass + await new Promise((resolve) => setTimeout(resolve, 100)) + + // then - notification SHOULD be sent (activity was within grace period, ignored) + expect(notificationCalls.length).toBeGreaterThanOrEqual(1) + }) + + test("should cancel notification for activity after grace period", async () => { + // given - main session is set + const mainSessionID = "main-grace-cancel" + setMainSession(mainSessionID) + + const hook = createSessionNotification(createMockPluginInput(), { + idleConfirmationDelay: 200, + skipIfIncompleteTodos: false, + activityGracePeriodMs: 50, + }) + + // when - session goes idle + await hook({ + event: { + type: "session.idle", + properties: { sessionID: mainSessionID }, + }, + }) + + // when - wait for grace period to pass + await new Promise((resolve) => setTimeout(resolve, 60)) + + // when - activity happens after grace period + await hook({ + event: { + type: "tool.execute.before", + properties: { sessionID: mainSessionID }, + }, + }) + + // Wait for original delay to pass + await new Promise((resolve) => setTimeout(resolve, 200)) + + // then - notification should NOT be sent (activity cancelled it after grace period) + expect(notificationCalls).toHaveLength(0) + }) }) diff --git a/src/hooks/session-notification.ts b/src/hooks/session-notification.ts index 3b3dcc514..00e171b7b 100644 --- a/src/hooks/session-notification.ts +++ b/src/hooks/session-notification.ts @@ -24,6 +24,8 @@ interface SessionNotificationConfig { /** Maximum number of sessions to track before cleanup (default: 100) */ maxTrackedSessions?: number enforceMainSessionFilter?: boolean + /** Grace period in ms to ignore late-arriving activity events after scheduling (default: 100) */ + activityGracePeriodMs?: number } export function createSessionNotification( ctx: PluginInput, From 8a827f9927a016a707a1d43208fdaaf841ede67b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:32:30 +0000 Subject: [PATCH 5/6] @acamq has signed the CLA in code-yeongyu/oh-my-openagent#2012 --- signatures/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/signatures/cla.json b/signatures/cla.json index 518dd4e69..8633fbd58 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -2015,6 +2015,14 @@ "created_at": "2026-03-07T13:53:56Z", "repoId": 1108837393, "pullRequestNo": 2360 + }, + { + "name": "crazyrabbit0", + "id": 5244848, + "comment_id": 3936744393, + "created_at": "2026-02-20T19:40:05Z", + "repoId": 1108837393, + "pullRequestNo": 2012 } ] } \ No newline at end of file From 9b4c826d0189f192bdcba283d26f616bd895d0e9 Mon Sep 17 00:00:00 2001 From: acamq <179265037+acamq@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:39:04 -0700 Subject: [PATCH 6/6] chore: restore bun.lock from dev --- bun.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index 458df7db2..e99fefc76 100644 --- a/bun.lock +++ b/bun.lock @@ -29,17 +29,17 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.11.0", - "oh-my-opencode-darwin-x64": "3.11.0", - "oh-my-opencode-darwin-x64-baseline": "3.11.0", - "oh-my-opencode-linux-arm64": "3.11.0", - "oh-my-opencode-linux-arm64-musl": "3.11.0", - "oh-my-opencode-linux-x64": "3.11.0", - "oh-my-opencode-linux-x64-baseline": "3.11.0", - "oh-my-opencode-linux-x64-musl": "3.11.0", - "oh-my-opencode-linux-x64-musl-baseline": "3.11.0", - "oh-my-opencode-windows-x64": "3.11.0", - "oh-my-opencode-windows-x64-baseline": "3.11.0", + "oh-my-opencode-darwin-arm64": "3.10.0", + "oh-my-opencode-darwin-x64": "3.10.0", + "oh-my-opencode-darwin-x64-baseline": "3.10.0", + "oh-my-opencode-linux-arm64": "3.10.0", + "oh-my-opencode-linux-arm64-musl": "3.10.0", + "oh-my-opencode-linux-x64": "3.10.0", + "oh-my-opencode-linux-x64-baseline": "3.10.0", + "oh-my-opencode-linux-x64-musl": "3.10.0", + "oh-my-opencode-linux-x64-musl-baseline": "3.10.0", + "oh-my-opencode-windows-x64": "3.10.0", + "oh-my-opencode-windows-x64-baseline": "3.10.0", }, }, }, @@ -238,27 +238,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.11.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-TLMCq1HXU1BOp3KWdcITQqT3TQcycAxvdYELMzY/17HUVHjvJiaLjyrbmw0VlgBjoRZOlmsedK+o59y7WRM40Q=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.10.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KQ1Nva4eU03WIaQI8BiEgizYJAeddUIaC8dmks0Ug/2EkH6VyNj41+shI58HFGN9Jlg9Fd6MxpOW92S3JUHjOw=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-szKfyAYbI3Mp6rqxHxcHhAE8noxIzBbpfvKX0acyMB/KRqUCtgTe13aic5tz/W/Agp9NU1PVasyqjJjAtE73JA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-PydZ6wKyLZzikSZA3Q89zKZwFyg0Ouqd/S6zDsf1zzpUWT1t5EcpBtYFwuscD7L4hdkIEFm8wxnnBkz5i6BEiA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QZ+2LCcXK6NPopYSxFCHrYAqLccN+jMQ0YrQI+QBlsajLSsnSqfv6W3Vaxv95iLWhGey3v2oGu5OUgdW9fjy9w=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-yOaVd0E1qspT2xP/BMJaJ/rpFTwkOh9U/SAk6uOuxHld6dZGI9e2Oq8F3pSD16xHnnpaz4VzadtT6HkvPdtBYg=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NZMbNG+kJ0FTS4u5xhuBUjJ2K2Tds8sETbdq1VPT52rd+mIbVVSbugfppagEh9wbNqXqJY1HwQ/+4Q+NoGGXhQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-pLzcPMuzBb1tpVgqMilv7QdsE2xTMLCWT3b807mzjt0302fZTfm6emwymCG25RamHdq7+mI2B0rN7hjvbymFog=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-f0GO63uAwzBisotiMneA7Pi2xPXUxvdX5QRC6z4X2xoB8F7/jT+2+dY8J03eM+YJVAwQWR/74hm5HFSenqMeIA=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ca61zr+X8q0ipO2x72qU+4R6Dsr168OM9aXI6xDHbrr0l3XZlRO8xuwQidch1vE5QRv2/IJT10KjAFInCERDug=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OzIgo26t1EbooHwzmli+4aemO6YqXEhJTBth8L688K1CI/xF567G3+uJemZ9U7NI+miHJRoKHcidNnaAi7bgGQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m0Ys8Vnl8jUNRE5/aIseNOF1H57/W77xh3vkyBVfnjzHwQdEUWZz3IdoHaEWIFgIP2+fsNXRHqpx7Pbtuhxo6Q=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ac7TfBli+gaHVu4aBtP2ADWzetrFZOs+h1K39KsR6MOhDZBl+B6B1S47U+BXGWtUKIRYm4uUo578XdnmsDanoA=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-a6OhfqMXhOTq1On8YHRRlVsNtMx84kgNAnStk/sY1Dw0kXU68QK4tWXVF+wNdiRG3egeM2SvjhJ5RhWlr3CCNQ=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OvOsPNuvZQug4tGjbcpbvh67tud1K84A3Qskt9S7BHBIvMH129iV/2GGyr6aca8gwvd5T+X05H/s5mnPG6jkBQ=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-lZkoEWwmrlVoZKewHNslUmQ2D6eWi1YqsoZMTd3qRj8V4XI6TDZHxg86hw4oxZ/EnKO4un+r83tb09JAAb1nNQ=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-fSsyVAFMoOljD+zqRO6lG3f9ka1YRLMp6rNSsPWkLEKKIyEdw1J0GcmA/48VI1NgtnEgKqS3Ft87tees1woyBw=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UqArUpatMuen8+hZhMSbScaSmJlcwkEtf/IzDN1iYO0CttvhyYMUmm3el/1gWTAcaGNDFNkGmTli5WNYhnm2lA=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-k9F3/9r3pFnUVJW36+zF06znUdUzcnJp+BdvDcaJrcuuM516ECwCH0yY5WbDTFFydFBQBkPBJX9DwU8dmc4kHA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BivOu1+Yty9N6VSmNzmxROZqjQKu3ImWjooKZDfczvYLDQmZV104QcOKV6bmdOCpHrqQ7cvdbygmeiJeRoYShg=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mRRcCHC43TLUuIkDs0ASAUGo3DpMIkSeIPDdtBrh1eJZyVulJRGBoniIk/+Y+RJwtsUoC+lUX/auQelzJsMpbQ=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BBv+dNPuh9LEuqXUJLXNsvi3vL30zS1qcJuzlq/s8rYHry+VvEVXCRcMm5Vo0CVna8bUZf5U8MDkGDHOAiTeEw=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],