From d466859b07011a2572ecb06d017baa107058b28f Mon Sep 17 00:00:00 2001 From: wangchen01 Date: Tue, 8 Sep 2026 17:49:46 +0800 Subject: [PATCH] fix(session-analysis): recognize lowercase edit tool names from Pi sessions Pi's coding agent emits lowercase tool names (edit, write), while the edit-event detection in episode-contract and insights matched Claude Code's capitalized names (Edit, Write) with a case-sensitive Set lookup. Every Pi edit event was therefore missed, which surfaced as withChanges: 0 in session facts for any workspace with real commits. Store the edit tool names lowercase and compare against the lowercased tool name, matching the existing case-insensitive check used for PATHLESS_EDIT_TOOL_NAMES in the same function. Validation: reran session-analysis facts --platform pi over a 254-session workspace; populationCoverage.withChanges moved 0 -> 29, candidate classes gained change-gap 11, and read-only-work dropped 44 -> 21 as editing episodes were no longer misclassified. The existing episode contract and lifecycle signal tests (28 tests, all using capitalized Claude tool names) pass unchanged, confirming backward compatibility. --- scripts/session-analysis/episode-contract.mjs | 17 ++++++++++------- scripts/session-analysis/insights.mjs | 5 +++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/session-analysis/episode-contract.mjs b/scripts/session-analysis/episode-contract.mjs index da48851e..f8dbf8cf 100644 --- a/scripts/session-analysis/episode-contract.mjs +++ b/scripts/session-analysis/episode-contract.mjs @@ -11,13 +11,16 @@ export const EVENT_SCHEMA_VERSION = 2; export const TASK_EPISODE_SCHEMA_VERSION = 3; export const DEFAULT_EPISODE_GAP_MS = 30 * 60 * 1000; +// Tool-name matching is case-insensitive: providers differ in casing +// (Claude Code emits "Edit"/"Write", pi emits "edit"/"write"), so entries +// are stored lowercase and compared against the lowercased tool name. const EDIT_TOOL_NAMES = new Set([ - "Edit", - "MultiEdit", - "NotebookEdit", - "NotebookWrite", - "SearchReplace", - "Write", + "edit", + "multiedit", + "notebookedit", + "notebookwrite", + "searchreplace", + "write", ]); const PATHLESS_EDIT_TOOL_NAMES = new Set(["apply_patch"]); const LIFECYCLE_USER_EVENT_TYPES = new Set(["user", "last-prompt", "UserPromptSubmit"]); @@ -131,7 +134,7 @@ export function isEditEvent(event) { if (event?.type === "event.patch_apply_end") return true; if (PATHLESS_EDIT_TOOL_NAMES.has(String(event?.toolName ?? event?.functionCallName ?? "").toLowerCase())) return true; if (/^\s*apply_patch(?:\s|$)/u.test(String(event?.commandText ?? ""))) return true; - return targetPaths.length > 0 && EDIT_TOOL_NAMES.has(event?.toolName); + return targetPaths.length > 0 && EDIT_TOOL_NAMES.has(String(event?.toolName ?? "").toLowerCase()); } export function validationCategory(event) { diff --git a/scripts/session-analysis/insights.mjs b/scripts/session-analysis/insights.mjs index a44abdac..c78aa5a1 100644 --- a/scripts/session-analysis/insights.mjs +++ b/scripts/session-analysis/insights.mjs @@ -31,7 +31,8 @@ const FRICTION_TYPE_PATTERNS = Object.freeze([ { name: "aborted-event", pattern: /\b(?:abort|aborted|cancel|cancelled|interrupted)\b/i }, ]); -const EDIT_TOOL_NAMES = new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]); +// Case-insensitive: providers differ in casing (Claude Code "Edit", pi "edit"). +const EDIT_TOOL_NAMES = new Set(["edit", "multiedit", "notebookedit", "write"]); const EDIT_COMMAND_PATTERNS = Object.freeze([ { name: "apply_patch", pattern: /\bapply_patch\b|\*\*\*\s+Begin Patch/i }, @@ -724,7 +725,7 @@ function longSessionRows(rows = []) { } function isEditEvent(event) { - if (EDIT_TOOL_NAMES.has(event.toolName)) { + if (EDIT_TOOL_NAMES.has(String(event?.toolName ?? "").toLowerCase())) { return true; } const commandText = event.commandText ?? "";