From c54000ece948f39951cac8bb3c67568266485695 Mon Sep 17 00:00:00 2001 From: z3347212573-cloud <306718766+z3347212573-cloud@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:54:50 +0800 Subject: [PATCH] fix(codex): cap SessionEnd hook timeout at Codex's 3s limit Codex clamps the SessionEnd hook timeout to 3s (the session is already exiting, so hooks may not drag on) and surfaces the clamped value as a hook loading problem in its config UI: clamping SessionEnd hook timeout to 3s in ~/.codex/hooks.json Memmy wrote the shared 60s timeout into every lifecycle hook, so the warning reappeared on every hook refresh and manual fixes were overwritten by the next install. Give codexHookEntries() an explicit timeout parameter and pass the 3s cap for SessionEnd only; SessionStart, PostCompact, UserPromptSubmit and Stop keep the 60s timeout. Fixes #381 --- .../outbound/skill-writer/codex/target.ts | 18 +++++++++++++++--- .../skill-writer/codex/tests/target.test.ts | 15 +++++++++++++++ .../agent-source/integration/codex/target.ts | 18 +++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/App/backend/src/adapters/outbound/skill-writer/codex/target.ts b/App/backend/src/adapters/outbound/skill-writer/codex/target.ts index 54e75627c..a4c020e51 100644 --- a/App/backend/src/adapters/outbound/skill-writer/codex/target.ts +++ b/App/backend/src/adapters/outbound/skill-writer/codex/target.ts @@ -28,6 +28,8 @@ const LEGACY_HOOK_SCRIPT_FILE_NAME = "memmy-memory-resume-hook.mjs"; const HOOK_CONFIG_FILE_NAME = "memmy-memory-config.json"; const WORKSPACE_BRIDGE_FILE_NAME = "memmy-workspace-bridge.mjs"; const HOOK_TIMEOUT_SECONDS = 60; +/** Codex caps SessionEnd hooks at 3s and reports the clamped value as a hook loading problem. */ +const SESSION_END_HOOK_TIMEOUT_SECONDS = 3; const START_MARKER = ""; const END_MARKER = ""; const LEGACY_CLI_START_MARKER = ""; @@ -233,7 +235,12 @@ async function upsertCodexHookConfig(filePath: string, hookCommand: string): Pro ]; hooks.SessionStart = codexHookEntries(hooks.SessionStart, hookCommand, "Loading Memmy world model"); hooks.PostCompact = codexHookEntries(hooks.PostCompact, hookCommand, "Updating Memmy world model"); - hooks.SessionEnd = codexHookEntries(hooks.SessionEnd, hookCommand, "Closing Memmy memory session"); + hooks.SessionEnd = codexHookEntries( + hooks.SessionEnd, + hookCommand, + "Closing Memmy memory session", + SESSION_END_HOOK_TIMEOUT_SECONDS + ); config.hooks = hooks; await writeFileAtomically(filePath, `${JSON.stringify(config, null, 2)}\n`); } @@ -273,10 +280,15 @@ async function removeCodexHookConfig(filePath: string): Promise { await writeFileAtomically(filePath, `${JSON.stringify(config, null, 2)}\n`); } -function codexHookEntries(value: unknown, hookCommand: string, statusMessage: string): Record[] { +function codexHookEntries( + value: unknown, + hookCommand: string, + statusMessage: string, + timeout: number = HOOK_TIMEOUT_SECONDS +): Record[] { return [ ...removeCodexResumeHookEntries(value), - { hooks: [{ type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, statusMessage }] }, + { hooks: [{ type: "command", command: hookCommand, timeout, statusMessage }] }, ]; } diff --git a/App/backend/src/adapters/outbound/skill-writer/codex/tests/target.test.ts b/App/backend/src/adapters/outbound/skill-writer/codex/tests/target.test.ts index 5ee80fafe..62e4f7961 100644 --- a/App/backend/src/adapters/outbound/skill-writer/codex/tests/target.test.ts +++ b/App/backend/src/adapters/outbound/skill-writer/codex/tests/target.test.ts @@ -127,6 +127,21 @@ describe("codex skill target", () => { } }); + it("caps the SessionEnd hook timeout at the Codex limit and keeps the other hooks at the default", async () => { + const { rootDirectory, memmyConfigPath } = createFixture(); + const target = createCodexSkillTarget({ rootDirectory, memmyConfigPath, trustHooks: noOpTrustHooks }); + + await target.installPlugin?.("codex"); + + const config = JSON.parse(readFileSync(join(rootDirectory, "hooks.json"), "utf8")) as { + hooks: Record }>>; + }; + expect(config.hooks.SessionEnd?.[0]?.hooks[0]).toMatchObject({ type: "command", timeout: 3 }); + expect(config.hooks.SessionEnd?.[0]?.hooks[0]?.command).toContain("memmy-resume-hook.mjs"); + expect(config.hooks.SessionStart?.[0]?.hooks[0]).toMatchObject({ timeout: 60 }); + expect(config.hooks.PostCompact?.[0]?.hooks[0]).toMatchObject({ timeout: 60 }); + }); + it("persists trust for the installed user-level hooks before installation completes", async () => { const { rootDirectory, memmyConfigPath } = createFixture(); let trustOptions: TrustMemmyCodexHooksOptions | undefined; diff --git a/Memory/src/agent-source/integration/codex/target.ts b/Memory/src/agent-source/integration/codex/target.ts index 54e75627c..a4c020e51 100644 --- a/Memory/src/agent-source/integration/codex/target.ts +++ b/Memory/src/agent-source/integration/codex/target.ts @@ -28,6 +28,8 @@ const LEGACY_HOOK_SCRIPT_FILE_NAME = "memmy-memory-resume-hook.mjs"; const HOOK_CONFIG_FILE_NAME = "memmy-memory-config.json"; const WORKSPACE_BRIDGE_FILE_NAME = "memmy-workspace-bridge.mjs"; const HOOK_TIMEOUT_SECONDS = 60; +/** Codex caps SessionEnd hooks at 3s and reports the clamped value as a hook loading problem. */ +const SESSION_END_HOOK_TIMEOUT_SECONDS = 3; const START_MARKER = ""; const END_MARKER = ""; const LEGACY_CLI_START_MARKER = ""; @@ -233,7 +235,12 @@ async function upsertCodexHookConfig(filePath: string, hookCommand: string): Pro ]; hooks.SessionStart = codexHookEntries(hooks.SessionStart, hookCommand, "Loading Memmy world model"); hooks.PostCompact = codexHookEntries(hooks.PostCompact, hookCommand, "Updating Memmy world model"); - hooks.SessionEnd = codexHookEntries(hooks.SessionEnd, hookCommand, "Closing Memmy memory session"); + hooks.SessionEnd = codexHookEntries( + hooks.SessionEnd, + hookCommand, + "Closing Memmy memory session", + SESSION_END_HOOK_TIMEOUT_SECONDS + ); config.hooks = hooks; await writeFileAtomically(filePath, `${JSON.stringify(config, null, 2)}\n`); } @@ -273,10 +280,15 @@ async function removeCodexHookConfig(filePath: string): Promise { await writeFileAtomically(filePath, `${JSON.stringify(config, null, 2)}\n`); } -function codexHookEntries(value: unknown, hookCommand: string, statusMessage: string): Record[] { +function codexHookEntries( + value: unknown, + hookCommand: string, + statusMessage: string, + timeout: number = HOOK_TIMEOUT_SECONDS +): Record[] { return [ ...removeCodexResumeHookEntries(value), - { hooks: [{ type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, statusMessage }] }, + { hooks: [{ type: "command", command: hookCommand, timeout, statusMessage }] }, ]; }