From 76e5d2e33a1ba29b2b5209d1c9008253b629e5b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 16:01:53 -0500 Subject: [PATCH 1/3] feat: add "what we now know" scar framing guidance to create_learning tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool descriptions and field hints now guide agents to frame scars as knowledge discoveries rather than self-criticism. Baseline measured: 34% explicit, 43% passive, 21% dead weight. Target: explicit ↑ 45%+, dead weight ↓ 10%. Co-Authored-By: Claude Opus 4.6 --- src/tools/definitions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index af3bd49..d4d12bf 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -170,7 +170,7 @@ export const TOOLS = [ }, { name: "create_learning", - description: "Create scar, win, or pattern entry in institutional memory", + description: "Create scar, win, or pattern entry in institutional memory. Frame as 'what we now know' — lead with the factual/architectural discovery, not what went wrong. Good: 'Fine-grained PATs are scoped to one resource owner'. Bad: 'Should have checked PAT type first'.", inputSchema: { type: "object" as const, properties: { @@ -181,11 +181,11 @@ export const TOOLS = [ }, title: { type: "string", - description: "Learning title", + description: "Frame as a knowledge discovery — what we now know. Lead with the factual insight, not self-criticism.", }, description: { type: "string", - description: "Detailed description", + description: "Detailed description. Include the architectural/behavioral fact that makes this retrievable by domain.", }, severity: { type: "string", @@ -895,7 +895,7 @@ export const TOOLS = [ }, { name: "gitmem-cl", - description: "gitmem-cl (create_learning) - Create scar/win/pattern in institutional memory", + description: "gitmem-cl (create_learning) - Create scar/win/pattern. Frame as 'what we now know' — factual discovery, not self-criticism.", inputSchema: { type: "object" as const, properties: { @@ -1553,7 +1553,7 @@ export const TOOLS = [ }, { name: "gm-scar", - description: "gm-scar (create_learning) - Create a scar/win/pattern in institutional memory", + description: "gm-scar (create_learning) - Create a scar/win/pattern. Frame as 'what we now know' — factual discovery, not self-criticism.", inputSchema: { type: "object" as const, properties: { From e1effd8ddbdceb94aae46d354f3b2d7ed1fd632e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 16:02:37 -0500 Subject: [PATCH 2/3] fix: auto-detect agent and session for scar usage tracking record_scar_usage and record_scar_usage_batch now fall back to detectAgent() and getCurrentSession() when caller omits agent/session_id. Reduces null entries in scar_usage table. Co-Authored-By: Claude Opus 4.6 --- src/tools/record-scar-usage-batch.ts | 10 ++++++++-- src/tools/record-scar-usage.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/tools/record-scar-usage-batch.ts b/src/tools/record-scar-usage-batch.ts index 6281513..b50c5ab 100644 --- a/src/tools/record-scar-usage-batch.ts +++ b/src/tools/record-scar-usage-batch.ts @@ -6,6 +6,8 @@ import { v4 as uuidv4 } from "uuid"; import * as supabase from "../services/supabase-client.js"; import { hasSupabase, getTableName } from "../services/tier.js"; +import { detectAgent } from "../services/agent-detection.js"; +import { getCurrentSession } from "../services/session-state.js"; import { Timer, recordMetrics, buildPerformanceData } from "../services/metrics.js"; import type { RecordScarUsageBatchParams, @@ -120,6 +122,10 @@ export async function recordScarUsageBatch( const resolvedScars = await Promise.all(resolutionPromises); + // Auto-detect agent and session as fallbacks for entries missing them + const fallbackAgent = detectAgent().agent || null; + const fallbackSessionId = getCurrentSession()?.sessionId || null; + // Build usage records for all successfully resolved scars const usageRecords = resolvedScars .filter(({ scarId }) => scarId !== null) @@ -130,8 +136,8 @@ export async function recordScarUsageBatch( scar_id: scarId, issue_id: entry.issue_id || null, issue_identifier: entry.issue_identifier || null, - session_id: entry.session_id || null, // Session tracking - agent: entry.agent || null, // Agent identity + session_id: entry.session_id || fallbackSessionId, + agent: entry.agent || fallbackAgent, surfaced_at: entry.surfaced_at, acknowledged_at: entry.acknowledged_at || null, referenced: entry.reference_type !== "none", diff --git a/src/tools/record-scar-usage.ts b/src/tools/record-scar-usage.ts index a8f9367..b6e5146 100644 --- a/src/tools/record-scar-usage.ts +++ b/src/tools/record-scar-usage.ts @@ -12,6 +12,8 @@ import { wrapDisplay } from "../services/display-protocol.js"; import * as supabase from "../services/supabase-client.js"; import { hasSupabase } from "../services/tier.js"; import { getStorage } from "../services/storage.js"; +import { detectAgent } from "../services/agent-detection.js"; +import { getCurrentSession } from "../services/session-state.js"; import { Timer, recordMetrics, @@ -33,13 +35,17 @@ export async function recordScarUsage( const metricsId = uuidv4(); const usageId = uuidv4(); + // Auto-detect agent and session if not provided by caller + const resolvedAgent = params.agent || detectAgent().agent || null; + const resolvedSessionId = params.session_id || getCurrentSession()?.sessionId || null; + const usageData: Record = { id: usageId, scar_id: params.scar_id, issue_id: params.issue_id || null, issue_identifier: params.issue_identifier || null, - session_id: params.session_id || null, // Session tracking - agent: params.agent || null, // Agent identity + session_id: resolvedSessionId, + agent: resolvedAgent, surfaced_at: params.surfaced_at, acknowledged_at: params.acknowledged_at || null, referenced: params.reference_type !== "none", From 5ef45826d4af8e34c305268e6f83f34f5758f411 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 07:51:44 -0500 Subject: [PATCH 3/3] fix: drop stale local-only threads on session_start when Supabase authoritative (OD-726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local-only threads (in threads.json but not in Supabase active set) were being unconditionally preserved, assuming they were mid-session creates not yet synced. In reality, most were resolved/archived in Supabase — the NOT-IN query excluded them, making them indistinguishable from truly local threads. Now only preserves local-only threads that have a valid ID and were created within the last 24h. Stale ghosts are dropped with a log message. Co-Authored-By: Claude Opus 4.6 --- src/tools/session-start.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/tools/session-start.ts b/src/tools/session-start.ts index a2477a1..d90d91b 100644 --- a/src/tools/session-start.ts +++ b/src/tools/session-start.ts @@ -732,13 +732,25 @@ function writeSessionFiles( let merged: ThreadObject[]; if (supabaseAuthoritative) { - // Supabase is source of truth — use its threads, but preserve any local-only threads - // (threads in the file that don't exist in the Supabase set, e.g. created via create_thread - // mid-session but not yet synced to Supabase by session_close). + // Supabase is source of truth — use its threads, but preserve local-only threads + // that were created recently (within 24h) and have a valid ID. These are likely + // mid-session threads not yet synced by session_close. Older local-only threads + // are stale — they were resolved/archived in Supabase but linger in the file + // because the NOT-IN query excludes them, making them look "local-only". const supabaseIds = new Set(threads.map(t => t.id)); - const localOnlyThreads = existingFileThreads.filter(t => !supabaseIds.has(t.id)); + const cutoff = Date.now() - 24 * 60 * 60 * 1000; + const localOnlyThreads = existingFileThreads.filter(t => { + if (supabaseIds.has(t.id)) return false; // exists in Supabase active set + if (!t.id) return false; // no ID = malformed, drop + const created = t.created_at ? new Date(t.created_at).getTime() : 0; + return created > cutoff; // only keep if created within last 24h + }); + const dropped = existingFileThreads.filter(t => !supabaseIds.has(t.id)).length - localOnlyThreads.length; if (localOnlyThreads.length > 0) { - console.error(`[session_start] Preserving ${localOnlyThreads.length} local-only threads not yet in Supabase`); + console.error(`[session_start] Preserving ${localOnlyThreads.length} recent local-only threads not yet in Supabase`); + } + if (dropped > 0) { + console.error(`[session_start] Dropped ${dropped} stale local-only threads (resolved/archived in Supabase)`); } merged = deduplicateThreadList([...threads, ...localOnlyThreads]); } else {