Skip to content

Commit d482b6d

Browse files
wbaikwesm
andauthored
fix: link Claude subagent tool calls (#459)
## Summary - Link Claude `Task` / `Agent` tool calls to child subagent sessions using result-side `toolUseResult.agentId` when queue/progress mapping is absent. - Preserve sibling Claude subagent tool calls when Claude emits additive same-`message.id` assistant chunks. - Keep streaming snapshot behavior by replacing progressive partial text with the latest text block while still preserving distinct additive content blocks. ## Context This is the Claude-side version of the inline subagent-linking contract addressed for Codex in #444 / #458. The UI already renders inline subagent sessions when `tool_calls.subagent_session_id` is populated. This PR makes Claude ingestion populate that existing field in more cases, so child sessions that are already ingested can appear inline under the exact parent `Task` / `Agent` tool call that launched them. ## Backfill note This changes parsed tool-call metadata for existing Claude rows. Existing archives will need a full parser reprocess / data-version bump to backfill inline links. This PR intentionally does not bump `dataVersion`; maintainers should decide whether to trigger that reparse. ## Validation - `go test ./internal/parser ./internal/sync -count=1` - `git diff --check origin/main...HEAD` If you add screenshots later, I’d add a final section: ## Visual Check - Before: Claude rendered the three sibling `Task` calls, but none had an inline linked subagent session. <img width="960" height="463" alt="image" src="https://github.com/user-attachments/assets/f0b0417e-476f-4763-ba43-dc49e469e4c9" /> - After: The same three sibling `Task` calls are preserved. <img width="959" height="514" alt="image" src="https://github.com/user-attachments/assets/f5c15e57-6737-41a0-8e33-71d3c610d10a" /> --------- Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
1 parent a9dd6d5 commit d482b6d

8 files changed

Lines changed: 1085 additions & 42 deletions

File tree

internal/db/db.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ import (
2727
// trigger a non-destructive re-sync (mtime reset + skip cache
2828
// clear) so existing session data is preserved.
2929
//
30-
// Bumped to 25: Codex parser now also links codex_app subagents
30+
// Bumped to 26: Claude parser now (a) links Task / Agent tool
31+
// calls to child subagent sessions via toolUseResult.agentId
32+
// when queue/progress mappings are absent, populating
33+
// tool_calls.subagent_session_id, and (b) merges additive
34+
// same-message.id assistant chunks instead of keeping only the
35+
// last entry, preserving sibling tool_use blocks and
36+
// progressively-built text. Existing rows need re-parsing so
37+
// these linkages and merged content show up.
38+
//
39+
// (25: Codex parser now also links codex_app subagents
3140
// via collab_agent_spawn_end event_msgs, wait_agent function
3241
// calls, and agent_path subagent notifications. Existing rows
33-
// need re-parsing so codex_app subagent linkage works.
42+
// need re-parsing so codex_app subagent linkage works.)
3443
//
3544
// (24: Codex parser now annotates spawn_agent tool calls
3645
// with subagent_session_id once the spawned agent id is known.
@@ -68,7 +77,7 @@ import (
6877
//
6978
// (17: Codex <skill> template filtering.)
7079
// (16: <turn_aborted> system messages.)
71-
const dataVersion = 25
80+
const dataVersion = 26
7281

7382
const tokenCoverageRepairStatsKey = "token_coverage_repair_v1"
7483

internal/db/messages.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,29 @@ func (db *DB) MaxOrdinal(sessionID string) int {
436436
return int(n.Int64)
437437
}
438438

439+
// LastClaudeMessageID returns the claude_message_id of the
440+
// highest-ordinal assistant message in a session whose
441+
// claude_message_id is non-empty, or "" if none exists. The sync
442+
// engine uses this to detect cross-sync splits of a single
443+
// streaming response (next sync's first appended assistant entry
444+
// shares the message.id of the previously-stored last assistant).
445+
func (db *DB) LastClaudeMessageID(sessionID string) string {
446+
var s sql.NullString
447+
err := db.getReader().QueryRow(
448+
`SELECT claude_message_id FROM messages
449+
WHERE session_id = ?
450+
AND role = 'assistant'
451+
AND claude_message_id != ''
452+
ORDER BY ordinal DESC
453+
LIMIT 1`,
454+
sessionID,
455+
).Scan(&s)
456+
if err != nil || !s.Valid {
457+
return ""
458+
}
459+
return s.String
460+
}
461+
439462
// savedPin captures the minimal pin state needed to re-attach a pin
440463
// after a full message replacement. source_uuid is the preferred
441464
// identifier because it survives rewrites where the ordinal stream

0 commit comments

Comments
 (0)