Skip to content

[Feat] Preserve structured Session history after native loss - #2990

Closed
roomote-roomote[bot] wants to merge 1 commit into
developfrom
feature/structured-opencode-restore-0baesq1y6l5zw
Closed

roomote-roomote[bot] wants to merge 1 commit into
developfrom
feature/structured-opencode-restore-0baesq1y6l5zw

Conversation

@roomote-roomote

@roomote-roomote roomote-roomote Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

​Opened on behalf of Daniel Riccio. Follow up by mentioning @roomote-roomote, in the web UI, or in Slack.

What changed

  • Capture an export-equivalent snapshot of a healthy Fast OpenCode parent Session after a settled assistant message. Capture is rejected when a tool is still pending/running, the expected completion is absent, the history exceeds 500 messages, or the serialized snapshot exceeds 8 MiB.
  • Store the snapshot encrypted at rest in fast_agent_conversations. Historical per-turn system prompts are removed, private Sessions redact all tool payloads, and shared Sessions redact personalization and integrations marked with private data policy while preserving tool identity and lifecycle state.
  • Claim and clear the snapshot when the next turn starts. OpenCode's existing native Session is still validated first. Only a native not-found result invokes the supported opencode import CLI path.
  • Clone imported Session, message, and part identities before import while preserving role order, assistant parent links, image/file parts, completed tool call/result IDs and states, nested tool attachments, and compaction tail links.
  • Continue an imported Session with only the new turn. Import failure or restored-session validation failure keeps the existing compatibility-history rebuild available; a failure after restored execution starts propagates instead of replaying current-turn side effects.
  • Add migration 0108 and focused SDK capture, encryption, snapshot lifecycle, recovery, stale/interrupted state, import failure, privacy redaction, image/tool, and compaction coverage.

Why this change was made

Fast Sessions already persist their last OpenCode Session ID, but a missing native store still falls back to flattened visible text. OpenCode 1.18.30 supports structured native restoration through its CLI import format, so this prototype preserves the healthy native representation and uses that supported path before accepting the lossy fallback.

This is intentionally separate from #2966 (historical image availability) and #2596 (canonical event projection and freshness semantics). It does not depend on either branch.

Impact

After a snapshot has been captured, a Fast Session whose local OpenCode state disappears can retain structured user/assistant roles, ordered parts, direct image input, completed tool results without re-executing those tools, and native compaction context. Existing Sessions and any bounded, stale, interrupted, unreadable, or failed snapshot continue through the explicit best-effort compatibility rebuild.

The snapshot is internal recovery material only. It is not added to transcript or API responses.

Prototype limitations

  • Existing Sessions have no snapshot until they complete a healthy turn after this migration.
  • Only the parent OpenCode Session is captured; child/subagent Session trees remain outside this prototype.
  • Oversized or unsettled histories deliberately skip snapshot persistence rather than truncating native structure.
  • A snapshot is one-shot claimed at the next turn and refreshed only after another healthy settled turn. If that turn is interrupted or capture fails, later native loss uses compatibility recovery.
  • Import is bounded to 30 seconds and creates fresh native IDs. A partial failed import is an unreachable local orphan and is never selected as the durable Session.
  • The prototype depends on the pinned OpenCode CLI and local OpenCode storage used by managed Fast helper servers. It does not add an upstream HTTP import endpoint.
  • Public tool payloads remain in the encrypted snapshot because they are model context; private tool payloads and historical personalized system prompts are removed before persistence.

Validation evidence

  • pnpm lint:fast passed.
  • pnpm check-types:fast passed across 27 packages.
  • pnpm knip passed with only the existing Mint configuration hint.
  • Cloud-agent Fast/runtime suite: 43 files passed, 1,110 tests passed, 1 skipped.
  • Focused recovery/capture/persistence suite: 532 tests passed.
  • Package TypeScript and format checks passed for @roomote/cloud-agents and @roomote/db.
  • Migration 0108 applied successfully to the local development database; the updated schema was pushed to the test database and real-Postgres assertions verified encrypted storage plus conditional one-shot claim.
  • The repository pre-push hook passed oxlint, residual lint, fast typechecking, and Knip.

Integrated disposable runtime smoke

An additional smoke ran the real answerFastAgentQuestion orchestration, native tool bridge, session manager, Postgres snapshot column, encryption/claim repository, and pinned opencode-ai@1.18.30 CLI in the local deployment. Only the OpenAI-compatible provider and Slack adapter were deterministic localhost stubs; no real model, live Slack, production data, or external side effect was used.

All 33 assertions passed:

  • A healthy image+tool turn stored an encrypted native snapshot through Roomote's actual database path. An ordinary next turn selected warm and did not repeat the historical reaction side effect.
  • After deleting only the disposable native OpenCode Session, logs showed cold_resume validation failure, Imported a healthy OpenCode snapshot, then successful cold_resume with no fallback_rebuild.
  • The provider boundary retained original role order, the data-URL PNG, assistant tool call call_counter_initial, and the matching role: "tool" result. The reaction counter remained one.
  • Native compaction was injected into the disposable source Session, captured by the next real Roomote turn, then recovered after another forced native loss. The provider received What did we do so far?, the summary, retained tail, and new turn.
  • A mismatched stale snapshot and malformed encrypted snapshot both skipped import and selected compatibility fallback.
  • A wrapper that failed only opencode import logged the import failure, selected compatibility fallback, and never made the partial fresh-ID import durable.
  • A forced HTTP 400 after a new post-restore reaction executed that current side effect once and terminated without compatibility fallback, proving the new turn was not replayed.
  • A forced interruption consumed the one-shot snapshot and left no usable stale snapshot.

Sanitized provider-boundary and recovery-log evidence: integrated smoke summary.

How to test

Automated

pnpm exec dotenvx run -f .env.test -- pnpm --filter @roomote/cloud-agents exec vitest run \
  src/server/fast-agent \
  src/server/__tests__/opencode-session-snapshot.test.ts \
  src/server/__tests__/opencode-runtime.test.ts \
  src/server/__tests__/non-task-provider-usage.test.ts
pnpm lint:fast
pnpm check-types:fast
pnpm knip

Disposable runtime recovery

  1. Apply migration 0108 and run a Fast Session through a turn containing a harmless completed tool call and an image.
  2. Verify fast_agent_conversations.opencode_snapshot is populated ciphertext and the normal transcript/API does not expose it.
  3. In a disposable environment, remove the local native OpenCode Session while leaving the persisted Roomote conversation and snapshot intact.
  4. Send a text-only follow-up. Confirm the logs show native validation failure followed by snapshot import, and that the imported Session receives only the new turn.
  5. Inspect the provider request: confirm the historical image and completed tool call/result are structured, the original tool call ID matches its result, and no historical action runs again.
  6. Repeat with a compacted source Session and confirm the summary plus retained tail are present after restore.
  7. Repeat with a deliberately failing OPENCODE_COMMAND; confirm the compatibility rebuild answers the turn and no partial imported Session becomes durable.

@roomote-community

roomote-community Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

No code issues found. See task

Reviewed 9f65721

@roomote-roomote
roomote-roomote Bot marked this pull request as ready for review September 19, 2026 18:43
@roomote-roomote roomote-roomote Bot closed this Oct 5, 2026
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