Skip to content

feat(plugin): session-recap injection — smart onset greeting from session history #523

Description

@jeonghun-jj-lee

Important

Problem: The onset router's personalized greeting depends on vault files (PROFILE.md, KNOWLEDGE.md, DEMOS.md, sessions/) that are only populated by power-user workflows (dream cycles, autoresearch campaigns). A normal user — even one with 170+ runs — gets a generic greeting with no context about what they've been working on.

Approach: At session start, the amicode_context plugin reads recent sessions (last 7 days) from the opencode session DB via the SDK client, summarizes each unsummarized session with a cheap LLM call, caches the recap, and injects a ## Recent sessions block into the system prompt. The model naturally uses this to personalize the onset greeting.

Approaches Considered:

  • (A) Session-start LLM summarization (chosen) — always fresh, works universally, captures intent and unfinished threads. Latency mitigated by per-session caching.
  • (B) End-of-session persist — no start-time latency but misses abandoned sessions; requires reliable "session end" signal.
  • (C) Mechanical extraction — zero LLM cost but can't identify unfinished threads or conversational intent; brittle heuristics.

Scope: opencode-plugin/ only (the Bun-runtime plugin layer). No changes to the onset router text, no changes to stack_state.ts, no changes to vault files.

Assumptions:

  • The opencode SDK client (from PluginInput) can call client.sessions.list() and client.sessions.history() from within the plugin.
  • The experimental.chat.system.transform hook supports async (returns Promise<void>) — confirmed from source.
  • A "small model" is available for summarization via the provider's cheaper tier (Haiku-class or similar).

Acceptance Criteria

  • A returning user with 1+ prior sessions in the last 7 days sees a ## Recent sessions section in the system prompt of their new session
  • Each session recap is 1-2 lines: topic, key outcomes (fidelity numbers, errors), and unfinished threads
  • Recaps are cached to ~/.amico/session-recaps/<session-id>.json — an already-summarized session is never re-summarized
  • The current session is excluded from the recap list
  • Sessions with parent_id set (subagent sessions) are excluded
  • If the LLM summarization fails (provider error, no API key), the hook degrades gracefully — no recap section, no crash
  • A fresh user with 0 prior sessions sees no ## Recent sessions block (clean empty state)
  • Total added latency on session start is < 3s for a user with 5 unsummarized sessions (parallel calls)

Key Decisions

Plugin architecture

The existing amicode_context.ts plugin gains session-recap capability. The plugin factory accepts PluginInput (currently ignored) and captures input.client. The hook becomes async.

amicode_context.ts (entry point)
  ├── buildStackStateBlock()           [existing, unchanged]
  └── buildRecentSessionsBlock(client) [new, from session_recap.ts]
        ├── client.sessions.list()     → filter: last 7 days, non-subagent, non-current
        ├── checkCache(id)             → ~/.amico/session-recaps/<id>.json
        ├── summarizeSession(client, id) → LLM call on message history
        └── composeMarkdown(recaps[])  → "## Recent sessions\n- ..."

LLM summarization contract

  • Model selection: Use the experimental.provider.small_model hook if available; otherwise use the user's default model. (This is a future-proof seam — when a small-model hook is wired, recaps become cheap automatically.)
  • Input: Text-type message parts from the session, truncated to ~4000 tokens (last messages first — recency matters more than opening).
  • Prompt: Fixed system prompt: "Summarize this Amicode session in 1-2 lines. Include: what was worked on, key numerical outcomes (fidelity values, iteration counts), and anything left unfinished or explicitly planned for next time. Be specific and concise."
  • Output: A single string, max ~200 tokens.

Cache format

{
  "session_id": "ses_...",
  "title": "Transmon X gate cold-start",
  "created": "2026-08-23T10:30:00Z",
  "recap": "Transmon X gate: launched 5 cold-start runs, best F=0.999954. Discussed min-time sweep to compress below 20ns.",
  "summarized_at": "2026-08-23T14:00:00Z"
}

Injected prompt section

## Recent sessions (last 7 days)

- **Aug 23, 10:30** — Transmon X gate: launched 5 cold-start runs, best F=0.999954. Discussed min-time sweep to compress below 20ns.
- **Aug 22, 14:00** — Onboarding: completed profile setup. Intent: automated experiments + insights.
- **Aug 21, 09:00** — Rydberg CZ debugging: identified 4th-order integrator needed. Not yet re-run.

Ordered by recency (newest first). Capped at 10 entries to bound prompt size (~1000 tokens max).

Constraints & Invariants

  • No vault dependency. This feature reads from the session DB only. It does not read or write vault markdown files. The existing vault-based sections (PROFILE.md, KNOWLEDGE.md, etc.) remain as-is for power users who run dream cycles.
  • Graceful degradation. If client.sessions.list() fails, or the LLM call fails, or the cache directory is unwritable — the hook returns without injecting a recap section. Never crash the prompt build.
  • Single-export constraint. The plugin file must have exactly one export (opencode's legacy-plugin scan). session_recap.ts is a sibling module imported by amicode_context.ts, not a separate plugin.
  • No onset router changes. The model already has instructions to "build the moment from the live state." The ## Recent sessions block IS live state — the router naturally uses it without text changes.
  • Exclude noise. Skip sessions shorter than 2 assistant messages (likely false starts). Skip sessions whose title starts with "Compaction" or similar internal housekeeping.

Prior Art

  • opencode-plugin/stack_state.ts — the existing pattern for reading state and composing a markdown block for prompt injection
  • opencode-plugin/amicode_context.ts — the existing hook entry point this extends
  • PR fix: onset router — state-aware options instead of first-run menu (#508) #509 (onset-router-state-aware) — introduced the state-aware option composition
  • The dream-distill skill — does a similar "summarize session transcripts" operation but writes to vault notes (heavyweight, offline)

Source

Brainstormed in Amicode session with JJ, 2026-08-23. The gap was identified by inspecting what data the onset router actually depends on (vault files populated only by dream cycles) versus what's universally available (the session DB).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions