Skip to content

fix(mcp): record_turn chat fallback + most-relevant-thread selection - #8

Merged
momo590 merged 2 commits into
mainfrom
fix/mcp-record-turn-and-thread-selection
May 21, 2026
Merged

momo590 merged 2 commits into
mainfrom
fix/mcp-record-turn-and-thread-selection

Conversation

@momo590

@momo590 momo590 commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two MCP bugs surfaced by the v0.2.3 + v0.3.0 combo on Claude Desktop chat:

Bug 1 — record_turn failed on every chat call

Two compounding causes:

  • _resolve_thread_id_for_write had no fallback once the v0.2.3 migration retired ~/.tinm/current_thread. Claude Desktop chat has no cwd, so every call without an explicit thread_id raised ValueError.
  • The success-string formatter dereferenced result['alpha_used'], but the v0.3.0 hot path moved alpha/update_count ownership to the async worker and no longer returns them → KeyError.

Fix:

  • Add a per-host chat-thread fallback persisted in ~/.tinm/chat-thread. Auto-created once on first use with origin=user, project_root=None. Disambiguates to claude-desktop-chat-<sha6> if the bare slug already belongs to a real project. Last-resort only — the explicit-slug path and the legacy CURRENT_FILE path are unchanged.
  • Make the formatter use .get() everywhere with a placeholder for missing alpha. Degrades cleanly on both v0.3.0 hot path and legacy sync path.

Bug 2 — Resume loaded the avant-dernier thread

Two root causes in _most_recent_thread:

  • 2a: Empty SessionStart auto-creates spawned by cron/autopilot from /root or /tmp had their last_updated bumped to "now" every minute, shadowing the user's real WIP thread.
  • 2b: Same-last_updated threads fell back to Path.glob() order — ext4 hash-table order, i.e. non-deterministic across runs.

Fix:

  • Replace the scalar timestamp comparison with a four-tuple relevance score (is_substantive, last_updated, n_user_turns, slug), extracted into _thread_relevance_score() so the read-path resolver can adopt the same ranking later.
  • Substance gate: is_substantive = 1 iff n_user_turns ≥ 1 AND last_updated != created_at. Empty autocreates score 0 and are only chosen when no substantive thread exists.
  • Slug as final tiebreaker eliminates the glob-order randomness.

Test plan

  • pytest mvp/tests/test_record_turn.py — 10 new tests, all green (resolver paths, chat-fallback create/persist/collision/recovery, legacy CURRENT_FILE precedence, formatter never crashes on missing alpha).
  • pytest mvp/tests/test_thread_selection.py — 12 new tests, all green (pure score helper + integration: substantive beats recent-empty, same-second multi-turn wins, only-autocreates falls back, corrupt/missing dirs handled).
  • pytest mvp/tests/ full suite — 420 passed, 1 pre-existing skip. No regressions.

Notes

  • Constraint adherence: two bug fixes in two separate commits on this branch.
  • Async worker / embedding code untouched.
  • ~/.tinm/current_thread was NOT reintroduced as the resolver — the new fallback uses a distinct per-host file (~/.tinm/chat-thread) that the read resolver never consults.

🤖 Generated with Claude Code

momo590 and others added 2 commits May 21, 2026 11:59
Two compounding bugs broke Claude Desktop chat's record_turn:

1. _resolve_thread_id_for_write raised ValueError on every chat call
   because the v0.2.3 migration retired ~/.tinm/current_thread and
   chat has no cwd to fall back to. Add a per-host chat-thread
   fallback persisted in ~/.tinm/chat-thread, auto-created once on
   first use with origin=user and project_root=None. Disambiguates
   to claude-desktop-chat-<sha6> if the bare slug is owned by a
   real project. Read resolver path is unchanged.

2. The success-string formatter dereferenced result['alpha_used'],
   but the v0.3.0 hot path moved alpha/update_count ownership to
   the async worker and no longer returns them. Use .get() with
   a '—' placeholder; degrades cleanly on both the v0.3.0 hot
   path and the legacy sync path.

Tests cover: explicit thread_id, chat-fallback create + persist +
collision disambiguation + bare-slug recovery, legacy CURRENT_FILE
still wins for pre-v0.2.3 installs, and the formatter never crashes
on missing alpha_used.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cent

_most_recent_thread used to win on max(last_updated) with a strict >
tiebreaker, so:

  2a. Empty SessionStart auto-creates spawned by cron / autopilot
      from /root or /tmp bumped their last_updated to "now" and
      shadowed the user's real work-in-progress.
  2b. Two threads with the same last_updated second fell back to
      Path.glob() order — ext4 hash-table order, i.e. non-
      deterministic across runs.

Replace the scalar timestamp comparison with a four-tuple relevance
score:

  (is_substantive, last_updated, n_user_turns, slug)

is_substantive = 1 iff the thread has ≥ 1 user turn AND
last_updated != created_at. Empty autocreates score 0 and are only
chosen when no substantive thread exists. n_user_turns + slug
provide a deterministic order for ties, eliminating the glob-order
randomness.

Logic factored into _thread_relevance_score so the read-path
resolver can adopt the same ranking later without copy/paste. This
commit only wires it into _most_recent_thread to keep the diff
narrow.

Tests cover: pure scoring (substantive beats recent-empty, multi-
turn beats single-turn at the same second, assistant-only turns
don't count as substantive, slug-deterministic ordering),
integration (substantive beats more-recent autocreate, only-
autocreates falls back to most-recent, corrupt JSON is skipped,
missing/empty threads dir returns None).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@momo590
momo590 merged commit d46c388 into main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant