Skip to content

refactor(threads): strip dead v1 message writes + migrate async-research stub to v2#4037

Open
tlgimenes wants to merge 1 commit into
mainfrom
strip-v1-message-writes
Open

refactor(threads): strip dead v1 message writes + migrate async-research stub to v2#4037
tlgimenes wants to merge 1 commit into
mainfrom
strip-v1-message-writes

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes/migrates v1 (thread_messages) message writes now that v2 (thread_message_parts, append-only stream-of-record) is the only live write path. Two parts:

A — delete the dead v1 write path (no behavior change)

After the Phase C cutover, SqlThreadStorage.saveMessages() was reachable only via Memory.save(), which has zero callers. Removed:

  • Memory.save() (api/routes/decopilot/memory.ts)
  • saveMessages from the storage port + impl + org-scoped wrapper (storage/ports.ts, storage/threads.ts)
  • the now-entirely-dead thread asset-hoisting proxy (object-storage/asset-hoister.ts) — its only real interception was saveMessages (kept connection/virtual-mcp hoisting)
  • the dead saveMessages (upsert) test block + inert saveMessages test mocks
  • stale doc-comments referencing the old saveMessagesToThread (harness packages, build-stream-request.ts, context-factory.ts)

v1 read paths (listMessages, listWithLastMessage, the v1 branches in memory.ts / list-messages.ts) are untouched — legacy v1 threads still render.

B — migrate the one live v1 writer (deep-research stub)

AsyncResearchJobs.markPolling() wrote a stub assistant message to thread_messages unconditionally so a browser refresh mid-poll can reconstruct the in-flight tool call. But v2 threads read thread_message_parts, so on v2 the stub went to a table nobody reads (refresh-recovery silently broken: tool-input-available parts are non-final → never persisted by the projector, and v2 loadWindow only surfaces messages with a finish anchor).

Now version-gated:

  • v2 thread → seed a tool_call part + finish anchor into thread_message_parts (run_id == threadId, deterministic ids, idempotent) so loadWindow surfaces it
  • legacy v1 thread → unchanged thread_messages stub
  • stub cleanup (deleteStubMessage on terminal transitions + sweepAbandoned) now clears both tables (best-effort)

Testing

  • bun run check — passes across all workspaces
  • bun run lint — 0 errors (3 pre-existing warnings in untouched files)
  • New storage/async-research-jobs.integration.test.ts (real PG): v2 markPolling seeds a folded, loadable assistant message with the tool-web_search input-available part; markCompleted removes it; v1 path still writes thread_messages.
  • Affected storage tests: 18 pass, 0 fail.

Follow-ups (not in this PR)

  • listWithLastMessage ("Suggested actions") reads thread_messages only — has no v2 equivalent (latent v2 gap).
  • Eventual v1 read removal would need a thread_messages → thread_message_parts backfill.

🤖 Generated with Claude Code


Summary by cubic

Removes dead v1 message writes and migrates the async-research polling stub to v2 thread_message_parts, fixing refresh recovery on v2 threads and simplifying storage.

  • Refactors

    • Removed Memory.save() and ThreadStoragePort.saveMessages (and all impls/wrappers).
    • Dropped the thread message asset-hoisting proxy; kept hoisting for connections and virtualMcps only.
    • Deleted obsolete tests and comments referencing saveMessages/inline persistence.
    • v1 read paths remain (legacy threads still render).
  • Bug Fixes

    • AsyncResearchJobs.markPolling() now seeds a v2 stub into thread_message_parts with a tool_call part and finish anchor; v1 threads unchanged.
    • Terminal cleanup and abandoned sweep delete stubs from both thread_messages and thread_message_parts.
    • Added PG integration tests covering v2 seed/cleanup and ensuring no v1 writes on v2 threads.

Written for commit 9f6dd95. Summary will update on new commits.

Review in cubic

…earch stub to v2

Two changes toward removing v1 (`thread_messages`) writes now that v2
(`thread_message_parts`, stream-of-record) is the only live write path:

A. Delete the dead v1 write path. `SqlThreadStorage.saveMessages()` and its
   sole (uncalled) caller `Memory.save()` were vestigial after the Phase C
   cutover. Removes: `Memory.save`, the `saveMessages` port method + impl +
   org-scoped wrapper, and the now-entirely-dead thread asset-hoisting proxy
   (its only real interception was `saveMessages`). Trims stale doc-comments
   referencing the removed `saveMessagesToThread`. No runtime behavior change.

B. Migrate the one remaining live v1 writer — the deep-research polling stub
   in `AsyncResearchJobs.markPolling()`. It wrote a v1 stub assistant message
   to `thread_messages` unconditionally, but v2 threads read from
   `thread_message_parts`, so the stub (which seeds refresh-recovery during the
   long async poll) was invisible on v2 threads. Now version-gated: v2 threads
   get a `tool_call` part + `finish` anchor written to `thread_message_parts`
   (so loadWindow surfaces it); legacy v1 threads keep the `thread_messages`
   stub. Stub cleanup (terminal transitions + abandoned sweep) now clears both
   tables. Adds an integration test covering v2 seed/cleanup and the v1 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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