LISAOS // DOCS
GATEWAY // PSYCHIC CACHE

Gateway Module — `psychic-cache/`

1. Purpose

The operational-memory tier — the short-lived, mission-scoped working store LISA and the CyberShinobi agents write to during a dispatch. Nine context_types (decision, directive, intel, state_change, output, report, escalation, shadow_clone, thread_checkpoint) each carry a structured Zod contract enforced at the write boundary. Entries are TTL-bounded (default 24h), per-mission token-budget-capped (10k) and entry-capped (200), and hybrid-searched (Voyage vector + FTS5 BM25 fused via RRF). The module also carries two correctness-load-bearing side channels: a dispatch heartbeat tick on every attributed write, and the thread_checkpoint compaction-survival store.

2. File Inventory

FileLinesResponsibility
repository.ts679createPsychicCacheRepository: write (budget/entry eviction, metadata JSON build, embed→psychic_vec, heartbeat tick, SSE emit), hybrid query (vector+FTS+RRF+filters+budget truncation), clear, stats, TTL cleanup, list, hasOutputForDispatch, getLatestCheckpoint
types.ts388PSYCHIC_CACHE_CONFIG, ContextType, CacheMetadata (flat union of all branch fields), WriteCacheInputSchema (9-branch z.discriminatedUnion with .strict()), QueryCacheInputSchema, ClearCacheInputSchema, CacheEntryListItem, repo interface
routes.ts157createPsychicCacheRouter: /write, /query, /checkpoint/:session_id, /all/entries, /:mission/entries, /:mission (DELETE), /:mission/stats
cleanup.ts43createTTLCleanupsetInterval (60-min) sweep of expired entries, unref'd
index.ts29createPsychicCacheLayer — assembles repo + router + ttlCleanup
Total1,296

3. Public API Surface

REST Endpoints

Mounted at /api/psychic-cache. Auth per AK-415: GETs → Bearer (write OR read-only); writes → Bearer (write only). No route is auth-exempt.

MethodPathAuthBody SchemaResponseSide Effects
POST/writeBearerWriteCacheInputSchema (9-branch union, strict)201 {data:{id, token_count, cache_total_tokens, evicted_count}} / 400Insert psychic_cache; FTS via AFTER-INSERT trigger; embed → psychic_vec; budget/entry eviction; heartbeat tick on agent_dispatch if dispatch_id; onWrite callback (dispatch GC sweep); SSE cache_write emit
POST/queryBearerQueryCacheInputSchema{data:{context_block, entries[], total_tokens, cache_size}} / 400Reads only; logs cache_query activity
GET/checkpoint/:session_idBearer{data: CacheEntryListItem} / 404Exact-match latest thread_checkpoint for session
GET/all/entriesBearer{data: CacheEntryListItem[]} (recent 100)none
GET/:mission/entriesBearer{data: CacheEntryListItem[]}none
DELETE/:missionBearer?reason / {reason} (read but not enforced){data:{deleted_count, freed_tokens}}Delete all mission rows + FTS + vec
GET/:mission/statsBearer{data: CacheStats}none

MCP Tools

Tool NameRaw payload?EndpointNotes
write_psychic_cacheNo (permissive-flat)POST /api/psychic-cache/writeLegacy; MCP schema exposes every branch field as flat optional
write_psychic_cache_rawYes ({payload})POST /api/psychic-cache/writeCanonical write path (D26) — staleness-immune one-field schema
clear_psychic_cacheNoDELETE /api/psychic-cache/:missionURL path + query only, no JSON body

Per-branch required-field enforcement + .strict() unknown-field rejection is performed server-side only (the MCP face is the permissive union of all branches). MCP handlers forward the full params via JSON.stringify(args).

4. Internal API

  • createPsychicCacheLayer(db, embeddingProvider, activityRepo?, onWrite?): {repo, router, ttlCleanup}.
  • PsychicCacheRepository methods consumed cross-module:
    • query() — wrapped by the context assembly psychicSearch adapter.
    • hasOutputForDispatch(missionId, dispatchId) — called by dispatch/routes.ts complete handler to set output_missing.
    • getLatestCheckpoint(sessionId) — backs the checkpoint GET route.
  • PSYCHIC_CACHE_CONFIG, WriteCacheInputSchema, ContextType — imported by dashboard-api, curator, tests.

5. Background Services

  • TTL cleanup (cleanup.ts) — setInterval every CLEANUP_INTERVAL_MS (60 min) calling repo.cleanupExpired() (deletes rows where expires_at < datetime('now') + FTS/vec). unref'd so it never keeps the process alive. Started from server/index.ts.
  • Token-budget eviction (on write) — when currentTokens + new > 10,000, evicts oldest-first until freed; also entry-count eviction at ≥ 200.
  • Dispatch heartbeat tick (D1175/AK-345) — on every write carrying dispatch_id, PK-bound UPDATE agent_dispatch SET last_heartbeat (status-filtered to live rows). Placed before the embedding await for latency. No-op for pre-dispatch LISA writes.
  • SSE timeline emitsetImmediate timelineBus.emitTimelineEvent({kind:'cache_write', …}) after commit.

6. Data Contracts

WriteCacheInputSchemaz.discriminatedUnion('context_type', [...]), each branch BaseWrite.extend({...}).strict().

BaseWrite = {mission_id≥1, content≥1, agent_name?, ttl_hours?>0, tags?[], source_turn?:int, dispatch_id?:int>0}.

Per-branch required fields:

  • decisiondecision_rationale; opt decision_alternatives[]
  • directivedirective_target; opt directive_priority enum
  • intelintel_source; opt intel_confidence enum
  • state_changestate_from, state_to; opt state_trigger
  • outputagent_name, output_url, dispatch_id (REQUIRED here only, AK-439/D1929); opt output_summary
  • reportreport_dispatch_id>0, report_outcome enum
  • escalationescalation_target, escalation_reason
  • shadow_clonesubtasks[](min 2); opt clone_count(1–3)
  • thread_checkpointsession_id, turn_count≥0, active_dispatches[], open_decisions[], pending_directives[], current_mission_namespaces[](min 1), working_state_prose(1–2000 chars)

QueryCacheInputSchema = {mission_id, query, token_budget?, context_types?[], tags?[]}. ClearCacheInputSchema = {mission_id, reason:enum} (defined but route reads reason without enforcing).

Owned tables (declared in memory/db.ts): psychic_cache (+2 indexes), psychic_vec (vec0, 512-dim), psychic_fts (fts5), and the 3 sidecar triggers psychic_cache_ai/ad/au_sidecar (FTS is trigger-maintained; service writes only vec).

7. Dependencies

  • Gateway modules consumed: memory/types (EmbeddingProvider), shared/search-utils (estimateTokens, buildFts5Query, reciprocalRankFusion), shared/zod-error (formatZodError), activity/repository (ActivityRepository), dispatch/eventBus (timelineBus). Writes to agent_dispatch (dispatch module's table) for heartbeat.
  • External libraries: better-sqlite3, sqlite-vec, express, zod.
  • Environment variables: none directly (TTL/budgets are PSYCHIC_CACHE_CONFIG constants; DB + embedding provider injected by the factory).

8. Test Coverage

LayerFileTests
Repository (CRUD, eviction, hybrid query, checkpoint, hasOutputForDispatch)test/psychic-cache-repository.test.ts35
Routes (7 endpoints, Zod negatives, auth boundary)test/psychic-cache-routes.test.ts42
Type/schema contracts (9-branch union, strict, per-field messages)test/psychic-cache-types.test.ts76

Strong coverage (153 tests) — the discriminated-union contract is the most heavily-tested surface in the shard.

9. Known Limitations

  • ClearCacheInputSchema.reason is validated nowhere — the DELETE route reads reason from query/body but does not run ClearCacheInputSchema, so an invalid/absent reason silently clears the mission. Cosmetic, low risk.
  • '*' mission sentinel returns empty — when the context psychic adapter passes no namespace it falls back to mission_id='*', which matches zero rows (documented D57). Harmless in practice (callers always pass a namespace) but the "search all missions" comment is misleading.
  • Live column-default drift — older DBs still default created_at to space-separated datetime('now'); mitigated by explicit ISO-Z binding on every insert (D52). Do not remove the explicit binding until the column default is formally migrated.
  • Recency owned by context funnelquery() returns pure RRF-ranked entries (D23 removed the internal recency boost); the direct /query REST consumer loses local recency bias.

10. Change History

DateDispatchSummary
2026-07-042295Initial per-module spec filed (audit campaign Phase 4)
(historical)D26write_psychic_cache_raw canonical write path (staleness fix)
(historical)D62thread_checkpoint + getLatestCheckpoint + checkpoint GET (Compaction Survival)
(historical)D71Structured metadata keys + flat CacheEntryListItem surfacing; full discriminated union
(historical)D77Universal dispatch_id attribution (base field)
(historical)D52Root-cause hasOutputForDispatch rewrite (metadata.dispatch_id key); NULL agent_name preservation
(historical)D1175 (AK-345)Dispatch heartbeat tick on attributed write
(historical)AK-439/D1929dispatch_id REQUIRED on output branch (close D52 asymmetry)

On this page