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 }] }, ]; }