From c74511291ced33352bd8a7dd6d4d5296d0118493 Mon Sep 17 00:00:00 2001 From: Daniel Bautista Date: Sat, 20 Jun 2026 13:49:47 -0600 Subject: [PATCH 1/4] feat(setup): add session end lifecycle for OpenCode, Gemini, and Codex (refers #48) --- internal/setup/plugins/opencode/engram.ts | 3 +++ internal/setup/setup.go | 1 + plugin/codex/skills/memory/SKILL.md | 1 + plugin/opencode/engram.ts | 3 +++ 4 files changed, 8 insertions(+) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index c5567087..1d8c085d 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -316,6 +316,9 @@ export const Engram: Plugin = async (ctx) => { const info = (event.properties as any)?.info const sessionId = info?.id if (sessionId) { + await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { + method: "POST", + }) toolCounts.delete(sessionId) knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 31333c1a..93c715c6 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -197,6 +197,7 @@ Before ending a session or saying "done" / "listo" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. +2. Call mem_session_end. ### PASSIVE CAPTURE — automatic learning extraction diff --git a/plugin/codex/skills/memory/SKILL.md b/plugin/codex/skills/memory/SKILL.md index d0ce3f50..adfaa7c6 100644 --- a/plugin/codex/skills/memory/SKILL.md +++ b/plugin/codex/skills/memory/SKILL.md @@ -111,6 +111,7 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. +2. Call `mem_session_end`. ## AFTER COMPACTION diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index c5567087..1d8c085d 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -316,6 +316,9 @@ export const Engram: Plugin = async (ctx) => { const info = (event.properties as any)?.info const sessionId = info?.id if (sessionId) { + await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { + method: "POST", + }) toolCounts.delete(sessionId) knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) From ef02c59fed2b971970549700ce3410c277639c75 Mon Sep 17 00:00:00 2001 From: Daniel Bautista Date: Mon, 22 Jun 2026 22:04:54 -0600 Subject: [PATCH 2/4] fix(opencode): avoid ending unpersisted sessions --- internal/setup/plugins/opencode/engram.ts | 11 +++++++---- internal/setup/setup.go | 2 +- plugin/codex/skills/memory/SKILL.md | 2 +- plugin/opencode/engram.ts | 11 +++++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 1d8c085d..444b4a23 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -110,6 +110,7 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. +2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. ### AFTER COMPACTION @@ -225,7 +226,6 @@ export const Engram: Plugin = async (ctx) => { if (!sessionId || knownSessions.has(sessionId)) return // Do not register sub-agent sessions in Engram (issue #116). if (subAgentSessions.has(sessionId)) return - knownSessions.add(sessionId) await engramFetch("/sessions", { method: "POST", body: { @@ -234,6 +234,7 @@ export const Engram: Plugin = async (ctx) => { directory: ctx.directory, }, }) + knownSessions.add(sessionId) } // Try to start engram server if not running @@ -316,9 +317,11 @@ export const Engram: Plugin = async (ctx) => { const info = (event.properties as any)?.info const sessionId = info?.id if (sessionId) { - await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { - method: "POST", - }) + if (knownSessions.has(sessionId)) { + await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { + method: "POST", + }) + } toolCounts.delete(sessionId) knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 93c715c6..15edfe81 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -197,7 +197,7 @@ Before ending a session or saying "done" / "listo" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call mem_session_end. +2. Call mem_session_end with the active session id and an optional summary of work completed. ### PASSIVE CAPTURE — automatic learning extraction diff --git a/plugin/codex/skills/memory/SKILL.md b/plugin/codex/skills/memory/SKILL.md index adfaa7c6..b0609149 100644 --- a/plugin/codex/skills/memory/SKILL.md +++ b/plugin/codex/skills/memory/SKILL.md @@ -111,7 +111,7 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call `mem_session_end`. +2. Call `mem_session_end` with the active session id and an optional summary of work completed. ## AFTER COMPACTION diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 1d8c085d..444b4a23 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -110,6 +110,7 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. +2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. ### AFTER COMPACTION @@ -225,7 +226,6 @@ export const Engram: Plugin = async (ctx) => { if (!sessionId || knownSessions.has(sessionId)) return // Do not register sub-agent sessions in Engram (issue #116). if (subAgentSessions.has(sessionId)) return - knownSessions.add(sessionId) await engramFetch("/sessions", { method: "POST", body: { @@ -234,6 +234,7 @@ export const Engram: Plugin = async (ctx) => { directory: ctx.directory, }, }) + knownSessions.add(sessionId) } // Try to start engram server if not running @@ -316,9 +317,11 @@ export const Engram: Plugin = async (ctx) => { const info = (event.properties as any)?.info const sessionId = info?.id if (sessionId) { - await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { - method: "POST", - }) + if (knownSessions.has(sessionId)) { + await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { + method: "POST", + }) + } toolCounts.delete(sessionId) knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) From 49097aae90c3638a0cc051c6a520804f6dcd89fe Mon Sep 17 00:00:00 2001 From: Daniel Bautista Date: Mon, 20 Jul 2026 11:13:15 -0600 Subject: [PATCH 3/4] fix(setup): keep session-close steps as a contiguous numbered list Move mem_session_end next to mem_session_summary so markdown no longer renders step 2 as an orphan after the summary template block. --- internal/setup/plugins/opencode/engram.ts | 6 ++++-- internal/setup/setup.go | 6 ++++-- plugin/codex/skills/memory/SKILL.md | 6 ++++-- plugin/opencode/engram.ts | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 444b4a23..93d62a7b 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -89,7 +89,10 @@ Also search memory PROACTIVELY when: ### SESSION CLOSE PROTOCOL (mandatory) Before ending a session or saying "done" / "that's it", you MUST: -1. Call \`mem_session_summary\` with this structure: +1. Call \`mem_session_summary\` with the structure below. +2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. + +\`mem_session_summary\` structure: ## Goal [What we were working on this session] @@ -110,7 +113,6 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. ### AFTER COMPACTION diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 15edfe81..db0fa4a3 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -176,7 +176,10 @@ Also search memory PROACTIVELY when: ### SESSION CLOSE PROTOCOL (mandatory) Before ending a session or saying "done" / "listo" / "that's it", you MUST: -1. Call mem_session_summary with this structure: +1. Call mem_session_summary with the structure below. +2. Call mem_session_end with the active session id and an optional summary of work completed. + +mem_session_summary structure: ## Goal [What we were working on this session] @@ -197,7 +200,6 @@ Before ending a session or saying "done" / "listo" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call mem_session_end with the active session id and an optional summary of work completed. ### PASSIVE CAPTURE — automatic learning extraction diff --git a/plugin/codex/skills/memory/SKILL.md b/plugin/codex/skills/memory/SKILL.md index b0609149..ca164f03 100644 --- a/plugin/codex/skills/memory/SKILL.md +++ b/plugin/codex/skills/memory/SKILL.md @@ -90,7 +90,10 @@ Also search memory PROACTIVELY when: ## SESSION CLOSE PROTOCOL (mandatory) Before ending a session or saying "done" / "that's it", you MUST: -1. Call `mem_session_summary` with this structure: +1. Call `mem_session_summary` with the structure below. +2. Call `mem_session_end` with the active session id and an optional summary of work completed. + +`mem_session_summary` structure: ## Goal [What we were working on this session] @@ -111,7 +114,6 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call `mem_session_end` with the active session id and an optional summary of work completed. ## AFTER COMPACTION diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 444b4a23..93d62a7b 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -89,7 +89,10 @@ Also search memory PROACTIVELY when: ### SESSION CLOSE PROTOCOL (mandatory) Before ending a session or saying "done" / "that's it", you MUST: -1. Call \`mem_session_summary\` with this structure: +1. Call \`mem_session_summary\` with the structure below. +2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. + +\`mem_session_summary\` structure: ## Goal [What we were working on this session] @@ -110,7 +113,6 @@ Before ending a session or saying "done" / "that's it", you MUST: - path/to/file — [what it does or what changed] This is NOT optional. If you skip this, the next session starts blind. -2. Call \`mem_session_end\` with the active session id and an optional summary of work completed. ### AFTER COMPACTION From 6e52fd5b3f4f3c47dc672edfb507dd03fd9f9973 Mon Sep 17 00:00:00 2001 From: Daniel Bautista Date: Mon, 20 Jul 2026 11:28:24 -0600 Subject: [PATCH 4/4] fix(opencode): require confirmed HTTP success for session tracking engramFetch ignored non-2xx responses, so failed creates still marked sessions known and failed /end calls dropped local tracking with no retry. --- internal/setup/plugins/opencode/engram.ts | 29 ++++++++++++++++++----- plugin/opencode/engram.ts | 29 ++++++++++++++++++----- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 93d62a7b..0e8ea89e 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -136,6 +136,9 @@ async function engramFetch( headers: opts.body ? { "Content-Type": "application/json" } : undefined, body: opts.body ? JSON.stringify(opts.body) : undefined, }) + // Treat non-2xx as failure so callers can avoid false-positive side effects + // (e.g. marking a session known when POST /sessions did not succeed). + if (!res.ok) return null return await res.json() } catch { // Engram server not running — silently fail @@ -228,7 +231,7 @@ export const Engram: Plugin = async (ctx) => { if (!sessionId || knownSessions.has(sessionId)) return // Do not register sub-agent sessions in Engram (issue #116). if (subAgentSessions.has(sessionId)) return - await engramFetch("/sessions", { + const created = await engramFetch("/sessions", { method: "POST", body: { id: sessionId, @@ -236,7 +239,10 @@ export const Engram: Plugin = async (ctx) => { directory: ctx.directory, }, }) - knownSessions.add(sessionId) + // Only mark known after confirmed create so failed requests can retry. + if (created != null) { + knownSessions.add(sessionId) + } } // Try to start engram server if not running @@ -320,12 +326,23 @@ export const Engram: Plugin = async (ctx) => { const sessionId = info?.id if (sessionId) { if (knownSessions.has(sessionId)) { - await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { - method: "POST", - }) + // session.deleted fires once — retry once inline, and only clear + // knownSessions after a confirmed /end (do not pretend it closed). + let ended = await engramFetch( + `/sessions/${encodeURIComponent(sessionId)}/end`, + { method: "POST" } + ) + if (ended == null) { + ended = await engramFetch( + `/sessions/${encodeURIComponent(sessionId)}/end`, + { method: "POST" } + ) + } + if (ended != null) { + knownSessions.delete(sessionId) + } } toolCounts.delete(sessionId) - knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) lastNudgeTime.delete(sessionId) } diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 93d62a7b..0e8ea89e 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -136,6 +136,9 @@ async function engramFetch( headers: opts.body ? { "Content-Type": "application/json" } : undefined, body: opts.body ? JSON.stringify(opts.body) : undefined, }) + // Treat non-2xx as failure so callers can avoid false-positive side effects + // (e.g. marking a session known when POST /sessions did not succeed). + if (!res.ok) return null return await res.json() } catch { // Engram server not running — silently fail @@ -228,7 +231,7 @@ export const Engram: Plugin = async (ctx) => { if (!sessionId || knownSessions.has(sessionId)) return // Do not register sub-agent sessions in Engram (issue #116). if (subAgentSessions.has(sessionId)) return - await engramFetch("/sessions", { + const created = await engramFetch("/sessions", { method: "POST", body: { id: sessionId, @@ -236,7 +239,10 @@ export const Engram: Plugin = async (ctx) => { directory: ctx.directory, }, }) - knownSessions.add(sessionId) + // Only mark known after confirmed create so failed requests can retry. + if (created != null) { + knownSessions.add(sessionId) + } } // Try to start engram server if not running @@ -320,12 +326,23 @@ export const Engram: Plugin = async (ctx) => { const sessionId = info?.id if (sessionId) { if (knownSessions.has(sessionId)) { - await engramFetch(`/sessions/${encodeURIComponent(sessionId)}/end`, { - method: "POST", - }) + // session.deleted fires once — retry once inline, and only clear + // knownSessions after a confirmed /end (do not pretend it closed). + let ended = await engramFetch( + `/sessions/${encodeURIComponent(sessionId)}/end`, + { method: "POST" } + ) + if (ended == null) { + ended = await engramFetch( + `/sessions/${encodeURIComponent(sessionId)}/end`, + { method: "POST" } + ) + } + if (ended != null) { + knownSessions.delete(sessionId) + } } toolCounts.delete(sessionId) - knownSessions.delete(sessionId) subAgentSessions.delete(sessionId) lastNudgeTime.delete(sessionId) }