ADR-011: In-memory Session State (No Persistence)

Status

Accepted

Context

The plugin stores state information during an opencode session: - toolCallCount — Number of tool calls since last reset - overrideCount — Number of bypass uses - maxOverrides — Maximum allowed bypasses - role — Active agent role - lastConfirmation — Timestamp of last confirmation

The question: Where does this state live? In-memory (volatile, lost on restart) or persistent (database, file)?

Alternatives Considered

Option A: In-memory (chosen)

Session state lives exclusively in the RAM of the opencode process.

class RuleEngine {
  private state: SessionState = {
    role: 'default',
    toolCallCount: 0,
    overrideCount: 0,
    maxOverrides: 3,
    lastConfirmation: null,
  }
}

Advantages: - Simple — no DB schema, no I/O, no latency - Fast — state access is <1ms (RAM vs. Disk/Network) - No external dependencies — no database, no file I/O - Privacy-friendly — state expires at session end, no PII on disk - GDPR-compliant — no persistent logs with personal data (see 02-architecture-constraints.md) - Fail-Open — no DB connection error possible

Disadvantages: - State is lost on opencode crash or restart - No cross-session state — toolCallCount starts at 0 each session - No audit trail — after session end, override counts are gone - No shared state between plugins (if multiple instances)

Option B: File-based Persistence (JSON/YAML)

State is written to a JSON file after each change.

Advantages: - State survives restart - Simple to implement (JSON.stringify + writeFile) - No external DB needed

Disadvantages: - I/O latency — every tool call would need to persist state - Race conditions — if opencode kills the process while writing - File locking — with parallel sessions (theoretically possible) - PII on disk — overrideCount + toolCallCount are not PII, but the audit context could be - No real benefit — opencode restarts the plugin for each session; state would be irrelevant after session end - Additional complexity for minimal benefit

Option C: SQLite / Database

State is persisted in a SQLite database.

Advantages: - Robust persistence - Query capability (audit: "how often was bypass used?")

Disadvantages: - Over-engineered — 4 integers + 1 string + 1 date = no justification for SQLite - Additional dependency (better-sqlite3 or similar) - I/O latency on every tool call - Not in scope (Architecture Constraints: "No database") - No use case — Audit logs are explicitly NOT in plugin scope (see 02-architecture-constraints.md: "Plugin must not be used for performance monitoring")

Evaluation Criteria

Criterion Weight Description

Simplicity

High

No additional components or dependencies

Speed

High

State access <1ms

Data privacy

Medium

No PII on disk

Persistence

Low

State does not need to survive session

Audit capability

Low

Audit is opencode’s task, not plugin

Decision

Option A: In-memory was chosen.

Rationale: - The plugin has no use case for persistent state: toolCallCount is purely session-related - Additional persistence would add complexity without measurable benefit - Data privacy: In-memory state expires at session end — no PII on disk - Performance: RAM access is <1ms (vs. Disk I/O or DB query) - Architecture Constraint: "No database" is already defined - Audit trail is not in plugin scope; opencode itself logs tool calls

What happens on restart?

Situation State Consequence

opencode exits normally

Lost

Next session starts at 0

opencode crashes

Lost

No data corruption (nothing to corrupt)

Plugin reloads

Lost

Config reload resets toolCallCount

Config reload (/anchor config-reload)

Reset

toolCallCount = 0, overrideCount = 0

opencode upgrade

Lost

No migration issues

State Structure

interface SessionState {
  role: string               // active role (default: "default")
  toolCallCount: number      // tool calls since last reset
  overrideCount: number      // bypass uses in this session
  maxOverrides: number        // from config (can change on reload)
  lastConfirmation: Date | null  // last "Weiter?" confirmation
}

Consequences

  • Positive: Maximum simplicity — no DB, no I/O, no file locks

  • Positive: Maximum performance — RAM access <1ms

  • Positive: Data privacy — no PII on disk, state expires with session

  • Positive: No additional dependencies

  • Negative: State is lost on opencode crash (last overrideCount gone)

  • Negative: No cross-session audit ("how often was bypass used today?")

  • Negative: Cross-session settings (e.g., "permanently change role") not possible

  • Trade-off: Persistence against simplicity — simplicity wins, because the use case does not require persistent state

  • SessionState in 05-building-block-view.md (Interface definition)

  • 06-runtime-view.md (Session Reset)

  • 02-architecture-constraints.md ("No database")

  • 01-introduction-and-goals.md (GDPR/Data Privacy — PII not persistent)

Sources