Skip to content

feat: structured tool call metadata and worktree project normalization - #18

Merged
wesm merged 17 commits into
mainfrom
worktree-tool-call-arguments
Feb 24, 2026
Merged

feat: structured tool call metadata and worktree project normalization#18
wesm merged 17 commits into
mainfrom
worktree-tool-call-arguments

Conversation

@wesm

@wesm wesm commented Feb 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Extract and persist structured tool call metadata (tool_use_id, input_json, skill_name, result_content_length) from session files into the tool_calls table
  • Pair tool_result content lengths back to their originating tool_call via tool_use_id in the sync engine, then filter empty user carrier messages
  • Expose structured tool_calls in the message API response and enrich frontend tool block rendering with parsed arguments (Bash commands, Task prompts, TaskCreate/Update metadata, Skill names, etc.)
  • Canonicalize worktree project names during import so archived worktree sessions group with the main repository even when worktree paths no longer exist on disk
  • Batch tool-call hydration queries to avoid SQLite bind-variable limits on large sessions

Test plan

  • All Go tests pass (CGO_ENABLED=1 go test -tags fts5 ./...)
  • All frontend tests pass (380 tests)
  • Worktree project normalization: online worktree, offline worktree with branch hint, offline worktree without branch
  • Tool-call batch hydration across batch boundaries (500+25 messages)
  • Manual: open a session with tool calls, confirm tool blocks show arguments and no empty user messages appear

🤖 Generated with Claude Code

clkao and others added 17 commits February 23, 2026 20:29
… type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…to tool_calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ToolResult type and ToolResults field to db.Message (transient)
- Update convertToolCalls to copy ToolUseID, InputJSON, SkillName
- Add convertToolResults to map ParsedToolResult to db.ToolResult
- Add pairToolResults to match result content lengths to tool calls
  by tool_use_id across message boundaries
- Call pairToolResults in writeBatch before ReplaceSessionMessages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ToolCall interface and tool_calls field to Message type
- Add "Skill" to TOOL_NAMES so Skill tool calls render as tool blocks
- Add enrichSegments() to attach structured tool_calls to parsed segments
- For Bash: replace truncated multi-line commands with full command from
  input_json, absorbing orphaned text fragments caused by regex \n\n boundary
- For Task: show prompt content and metadata (subagent_type, description)
  when the tool block is expanded
- Pass toolCall prop through ToolBlock, MessageContent, ToolCallGroup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prose text in assistant messages can contain [ToolName: args] patterns
that TOOL_RE would falsely parse as tool blocks. Pass has_tool_use to
parseContent and skip the TOOL_RE loop when the flag is false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…endMessage

Go backend: specific formatters in formatToolUse for task management
and messaging tools, replacing generic [Tool: name] output.

Frontend: add new tool names to TOOL_NAMES regex, add metadata
display for TaskCreate (subject/description) and TaskUpdate
(taskId/status) in ToolBlock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User messages whose content is only tool_result blocks were being skipped
because ExtractTextContent produces no text for them, causing the sync
engine's pairToolResults to never see the result content lengths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User messages containing only tool_result blocks have empty Content.
These were preserved so pairToolResults() could match them to tool_calls,
but they should not appear in the UI. The new pairAndFilter() function
pairs results first, then removes messages with no displayable content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- canonicalize project names using cwd + git branch hints so archived worktree sessions group with the main repo even when worktree paths are gone\n- keep filesystem-based git root resolution when available and add parser/sync coverage for both online/offline worktree cases\n- batch tool_call hydration queries to avoid large IN-clause limits and add a regression test across batch boundaries
- pairAndFilter now only removes user messages that have
  ToolResults and empty content, preserving assistant messages
  and user messages without tool results
- use disjoint cache key prefixes in parseContent to prevent
  theoretical collision between tools/notools modes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wesm

wesm commented Feb 24, 2026

Copy link
Copy Markdown
Member Author

cc @clkao — this superseds your PR since I rebased and pushed some other stuff

@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (551c7dc)

Summary Verdict: The PR introduces several High and Medium severity issues, primarily involving the
potential exposure of sensitive tool arguments, a missing database column causing runtime SQL errors, and logic bugs in project extraction and content parsing.

High Severity

1. Sensitive Tool Inputs Exposure

  • Files: internal/parser/content.go:49, internal/db/messages.go :27, internal/db/messages.go:252, frontend/src/lib/utils/content-parser.ts:202, frontend/src/lib/components/content/ToolBlock.svelte:103
  • Description: Raw tool inputs
    (input_json) are extracted, stored, returned in API payloads, and rendered in the UI (including multiline commands and prompts). These inputs commonly contain secrets (tokens, passwords, private paths), significantly expanding the credential exposure surface.
  • Suggested Remediation: Redact sensitive fields/flags before persistence (
    token, password, authorization, -p, --api-key, etc.), store only allowlisted fields for display, and gate full input_json behind explicit privileged access (or disable by default).

2. Missing Database Column Runtime Error

  • Files: internal/db/ messages.go (around line 407)
  • Description: The query uses ORDER BY id on the tool_calls table, but the schema for tool_calls does not define an id column. This will cause a SQLite syntax error (no such column: id) during runtime.
  • Suggested Remediation: Use ORDER BY rowid or explicitly add an id primary key column to the tool_calls table schema.

Medium Severity

1. Unintentional Tool Metadata Exposure

  • Files: internal/db/messages .go:100, internal/db/messages.go:123
  • Description: GetMessages(..., includeContent bool) and GetAllMessages always call attachToolCalls, regardless of the includeContent flag. If callers rely on includeContent=false for reduced/safe output, sensitive tool arguments can still be exposed via tool_calls.input_json.
  • Suggested Remediation: Respect the content/redaction mode for tool_calls as well (skip attaching, or attach only non-sensitive metadata when includeContent=false).

2. Out-of-Bounds Panic in Branch Suffix Trimming (DoS)

  • Files: internal/parser/project.go (around line 204)
  • Description: trimBranchSuffix slices the original string based on the byte length of a lowerc
    ased suffix. Because strings.ToLower can alter the byte length of certain Unicode characters, len(name) - len(suffix) can become negative, causing an immediate runtime panic or malformed UTF-8. A malicious actor can trigger this panic via session logs.
  • Suggested Remediation: Locate
    the suffix boundary using case-insensitive methods that return the correct byte index in the original string, use a case-insensitive regular expression, or use strings.TrimSuffix(strings.ToLower(name), ...) if preserving casing is not strictly necessary.

3. Incorrect Project Git Root Fallback

Files: internal/parser/project.go:73, internal/parser/project.go:105

  • Description: ExtractProjectFromCwdWithBranch always tries findGitRepoRoot(cleaned) first. When cwd no longer exists, find GitRepoRoot falls back to filepath.Dir(cwd) and may latch onto an unrelated parent git repository on the current machine, overriding the intended branch-suffix fallback.
  • Suggested Remediation: Only probe the git root when cwd itself exists (or at least when the path can be validated
    ), otherwise skip directly to basename + trimBranchSuffix.

4. Incorrect Bash Orphan-Fragment Absorption

  • Files: frontend/src/lib/utils/content-parser.ts:227
  • Description: In enrichSegments, Bash orphan-fragment absorption uses
    fullCmd.includes(next.content.trim()). This substring match can incorrectly drop legitimate following text segments that happen to be substrings of the command.
  • Suggested Remediation: Absorb only a strict continuation fragment (e.g., prefix/suffix-based matching against the expected remainder), rather than any
    substring match.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

@wesm

wesm commented Feb 24, 2026

Copy link
Copy Markdown
Member Author

These findings are false positives, merging!

@wesm
wesm merged commit 4142c33 into main Feb 24, 2026
6 checks passed
cursor Bot referenced this pull request in diazMelgarejo/periscope Jun 1, 2026
#18)

## Summary

- Extract and persist structured tool call metadata (`tool_use_id`,
`input_json`, `skill_name`, `result_content_length`) from session files
into the `tool_calls` table
- Pair `tool_result` content lengths back to their originating
`tool_call` via `tool_use_id` in the sync engine, then filter empty user
carrier messages
- Expose structured `tool_calls` in the message API response and enrich
frontend tool block rendering with parsed arguments (Bash commands, Task
prompts, TaskCreate/Update metadata, Skill names, etc.)
- Canonicalize worktree project names during import so archived worktree
sessions group with the main repository even when worktree paths no
longer exist on disk
- Batch tool-call hydration queries to avoid SQLite bind-variable limits
on large sessions

## Test plan
- [x] All Go tests pass (`CGO_ENABLED=1 go test -tags fts5 ./...`)
- [x] All frontend tests pass (380 tests)
- [x] Worktree project normalization: online worktree, offline worktree
with branch hint, offline worktree without branch
- [x] Tool-call batch hydration across batch boundaries (500+25
messages)
- [ ] Manual: open a session with tool calls, confirm tool blocks show
arguments and no empty user messages appear

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: CL Kao <clkao@datarecce.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants