From ac538427013b397cb23252db36892c92ee0f4208 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Wed, 20 May 2026 16:47:30 +0100 Subject: [PATCH] fix(observe): auto-create session row + surface session/start HTTP errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes a silent data-loss path: when an agent hook calls POST /agentmemory/observe before (or instead of) a successful POST /agentmemory/session/start, observations were written to KV.observations(sessionId) but KV.sessions never got the parent row. GET /agentmemory/sessions then returned an empty list even though the underlying observation files were on disk. Two complementary changes: 1. Server-side (src/functions/observe.ts) — when observe lands and the session row is missing, insert it inline using the project / cwd / timestamp / first prompt already present on the payload. session/start remains the fast path (it also returns the inject-context block) but is no longer load-bearing for durability. 2. Hook-side (src/hooks/session-start.ts) — explicit stderr write when session/start returns a non-2xx status, on both the fire-and-forget and inject-context paths. Network / timeout errors stay quiet (common on cold-start); HTTP errors surface so wiring problems (wrong URL, auth, etc.) become visible to the user. The compiled plugin/scripts/session-start.mjs is regenerated by tsdown. Tests (1081) + build pass. --- plugin/scripts/session-start.mjs | 4 +++- src/functions/observe.ts | 22 ++++++++++++++++++++++ src/hooks/session-start.ts | 24 +++++++++++++++++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 9e573e24..12e6d91e 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -41,6 +41,8 @@ async function main() { fetch(url, { ...init, signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS) + }).then((res) => { + if (!res.ok) process.stderr.write(`[agentmemory] session/start returned ${res.status} for ${sessionId}\n`); }).catch(() => {}); return; } @@ -52,7 +54,7 @@ async function main() { if (res.ok) { const result = await res.json(); if (result.context) process.stdout.write(result.context); - } + } else process.stderr.write(`[agentmemory] session/start returned ${res.status} for ${sessionId}\n`); } catch {} } main(); diff --git a/src/functions/observe.ts b/src/functions/observe.ts index 57e2f7df..092de67b 100644 --- a/src/functions/observe.ts +++ b/src/functions/observe.ts @@ -214,6 +214,28 @@ export function registerObserveFunction( } } await kv.update(KV.sessions, payload.sessionId, updates); + } else { + // Auto-create the session row when an observation arrives before + // (or instead of) a successful session/start call. The hook + // scripts call session/start as fire-and-forget; on transient + // server unavailability that POST is silently dropped, leaving + // observations orphaned from GET /agentmemory/sessions even + // though they're present on disk. Re-creating here closes the + // data-loss hole permanently; session/start remains the fast + // path (it also returns the injected context). + const firstPrompt = + typeof raw.userPrompt === "string" + ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) + : undefined; + await kv.set(KV.sessions, payload.sessionId, { + id: payload.sessionId, + project: payload.project, + cwd: payload.cwd, + startedAt: payload.timestamp, + status: "active", + observationCount: 1, + ...(firstPrompt ? { firstPrompt } : {}), + }); } // Per-observation LLM compression is opt-in as of 0.8.8 (#138). diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index a6cefe41..bd7c58e6 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -62,11 +62,24 @@ async function main() { if (!INJECT_CONTEXT) { // Pure telemetry path: caller never reads the response, so don't // block on it. AbortSignal.timeout caps the wait the event loop - // gives the pending socket before exit. + // gives the pending socket before exit. Server-side observe now + // auto-creates a session row when one is missing (#522), so a + // dropped POST here is a missed context-inject, not data loss. fetch(url, { ...init, signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS), - }).catch(() => {}); + }) + .then((res) => { + // Surface HTTP errors so wiring problems (wrong URL, auth) + // don't stay invisible. Network/timeout errors fall through + // to .catch() and stay quiet — those are common on cold-start. + if (!res.ok) { + process.stderr.write( + `[agentmemory] session/start returned ${res.status} for ${sessionId}\n`, + ); + } + }) + .catch(() => {}); return; } @@ -80,9 +93,14 @@ async function main() { if (result.context) { process.stdout.write(result.context); } + } else { + process.stderr.write( + `[agentmemory] session/start returned ${res.status} for ${sessionId}\n`, + ); } } catch { - // silently fail -- don't block Claude Code startup + // network/timeout — don't block Claude Code startup; observe-side + // will still pick up the session via auto-upsert (#522). } }