Skip to content

Latest commit

 

History

History
1276 lines (1008 loc) · 94.7 KB

File metadata and controls

1276 lines (1008 loc) · 94.7 KB

AGENTS.md — TIMPS Codebase Guide

Canonical instruction doc for AI agents (OpenCode, Copilot, Claude, Cursor). If this conflicts with the README, trust this file.

Repo layout

Monorepo workspace roots: packages/*, apps/*, timps-code, timps-mcp.

| Surface | Path | Package | Build | Test | Typecheck | |---|---|---|---|---|---|---| | CLI | timps-code/ | timps-code | tsc (ESM, NodeNext) | vitest (unified) | tsc --noEmit | | MCP server | timps-mcp/ | timps-mcp | tsup src/index.ts --format cjs --no-dts --out-dir dist | vitest (unified) | tsc --noEmit (4GB heap) | | VS Code ext | timps-vscode/ | timps-ai-coding-agent | npm run compile (tsc) | vitest (unified) | tsc --noEmit | | Full server | packages/server/ | @timps/server | tsc (CJS) | vitest (unified) | tsc --noEmit | | Memory engine | packages/memory-core/ | @timps/memory-core | tsup (CJS + dts) | vitest (unified) | tsc --noEmit |

  • npm run build — turbo run build (all packages; mobile/plugins/docs may need extra tooling).
  • npm run build:ci — CI-targeted build that excludes 10 packages (mobile, plugins, docs, timps-code, timps-mcp).
  • Build timps-code/timps-mcp individually: cd timps-code && npm run build.

TypeScript config drift

  • timps-code → ESM (module: NodeNext), .js extension in imports required.
  • timps-mcp → CJS, strict: false, no declarations.
  • packages/server → CJS, wide include.
  • packages/memory-core → CJS, vitest resolves .js imports automatically.

Agent entrypoints (timps-code)

  • CLI: src/bin/timps.ts (run via npm run dev or tsx src/bin/timps.ts).
  • Loop: src/core/app.tsAgentLoop.run()src/core/agent.ts.
  • Tool registry: src/tools/tools.ts exports ALL_TOOLS, getTool(), getToolDefinitions().
  • Providers: src/models/ — 6 adapters + OpenRouter routing (7 total, not "75+").
  • Swarm: src/swarm/ — 10 agent roles, DAG is local fan-out, not distributed.
  • TUI: src/ui/App.tsx (Ink/React 19).
  • MCP client: src/services/mcp/, auto-discovery at src/tools/mcpDiscovery.ts.

Auto-capture & context-anywhere (timps setup / recall)

  • timps setup installs MCP registration and a marker-fenced instruction block (<!-- timps:memory:start --><!-- timps:memory:end -->) into each detected agent's global rule file, so agents pull context at session start and store user data proactively. Files: ~/.claude/CLAUDE.md, ~/.config/opencode/AGENTS.md, ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md, Cursor ~/.cursor/rules/timps.mdc (alwaysApply: true frontmatter). Windsurf has no instruction file (MCP only). See timps-code/src/commands/setup.ts (installInstructions/uninstallInstructions).
  • Idempotent + reversible: re-running timps setup is a no-op; --uninstall removes the block; --no-instructions skips it; --dry-run previews. --binary <path> points at a specific MCP binary (default is npx -y @timps-ai/timps-mcp, which only works once published).
  • CLI commands in timps-code/src/commands/recall.ts (wired in src/bin/timps.ts):
    • timps recall "<query>" [--limit N] [--project <path>] — search the shared store from any terminal.
    • timps context [--project <path>] — print the full memory context string.
  • timps setup --server <url> sets both TIMPS_URL and TIMPS_MEMORY_URL (in buildRegistration()), because timps-mcp's memory tools (timps_store_memory, timps_get_memories, timps_check_contradiction) route through MemoryClient only when TIMPS_MEMORY_URL is set AND server mode is on (TIMPS_URL). Override either via TIMPS_SETUP_ENV=TIMPS_MEMORY_URL=… timps setup …. Legacy packages/server (port 3000) is the only thing that serves /api/* (timpsAPI chat proxy); a MemoryServer-only deploy 404s those non-memory tools.
  • MemoryServer deploy: full compose stack (Postgres primary + 2 streaming replicas, PgBouncer, Redis, Qdrant, MemoryServer ×N, Prometheus, Grafana, OTel) lives in packages/memory-core/docker-compose.yml. Guide: DEPLOY.md → Option 5. Single-process: MEMORY_PROJECT_PATH=<dir> MEMORY_PORT=4100 node packages/memory-core/dist/server/start.js (defaults /data, which must exist/writable). Entry dist/server/start.js mounts eval routes → needs writable project path at startup.
  • projectHash() canonicalizes with fs.realpathSync so a project reachable via multiple path spellings (e.g. /tmp/… vs /private/tmp/… on macOS) maps to one store under ~/.timps/memory/<hash>/. Keep this behavior if you touch packages/memory-core/src/storage.ts (that package is git-ignored/hidden here; rebuild its dist so timps-code picks up the change).
  • End-to-end contract: agents store via timps_store_memory MCP (project dir → canonical hash), terminal reads via timps recall/timps context on the same canonical hash — they always converge on one store.

Memory — unified implementations

  1. packages/memory-core/ — canonical, source of truth for 25 intelligence tools.
  2. timps-code/src/memory/ — thin adapter (337 lines), delegates to MemoryEngine.
  3. packages/server/memory/ — thin adapters over MemoryEngine, re-export forge types from @timps/memory-core.

22 forge layers: L1 Working → L2 Episodic → L3 Semantic → L4 Procedural → L5 ChronosForge → L6 ResonanceForge → L7 EchoForge → L8 AetherForgeERL → L9 HarmonicSheafWeaver → L10–L22 (EngramLog through BiasRevealer).

All 25 intelligence tools live in packages/memory-core/src/intelligence/, class-based, no Math.random().

IMemoryLayer interface

All forge classes implement IMemoryLayer (defined in packages/memory-core/src/IMemoryLayer.ts):

Forge File store→storeData renamed? Notes
EchoForge EchoForge.ts ✅ Yes First IMemoryLayer implementation
ChronosForge ChronosForge.ts N/A (no conflict) Weave-based store
HarmonicSheafWeaver HarmonicSheafWeaver.ts ✅ Yes Persists via persist()
AetherForgeERL AetherForgeERL.ts ✅ Yes Persists via persist()

All provide: store(), retrieve(), verify(), contradict(), archive(), getProvenance(), explain(), audit(), decay().

Gotcha: When implementing IMemoryLayer on a forge with a private store field, rename it to private storeData to avoid method/property conflict. Update ALL this.store.this.storeData. references and any test code accessing forge['store'] (via bracket notation) to forge['storeData'].

StorageBackend abstraction (Phase 1b)

All forge layers and intelligence tools accept an optional backend?: StorageBackend parameter after dir. When provided, all file I/O routes through the backend instead of direct fs calls.

Interface defined in packages/memory-core/src/backends/types.ts:

read(key), write(key, value), delete(key), list(prefix?),
query(filter), exists(key), append(key, line), beginTxn()
Backend File Sync/Async Driver Notes
FileBackend backends/FileBackend.ts sync fs Default. WAL journaling: write → .wal → fsync → rename
InMemoryBackend backends/InMemoryBackend.ts sync Map For tests
PostgresBackend backends/PostgresBackend.ts async pg Lazy-loaded, key/value JSONB table
SQLiteBackend backends/SQLiteBackend.ts sync better-sqlite3 Lazy-loaded, WAL mode
RedisBackend backends/RedisBackend.ts async ioredis Lazy-loaded, STRING + SET

Usage:

const engine = new MemoryEngine(dir, { backend: new InMemoryBackend() });
// Forge layers automatically use engine._backend

When no backend is provided, MemoryEngine creates a FileBackend via getBackend(dir) in storage.ts. The getBackend() cache is shared across all forges using the same directory.

Gotcha: The Rust native addon (@timps/memory-core-rs) writes episodes in JSONL format (episodes.jsonl), but the backend uses JSON array (episodes.json). To prevent format mismatch, all storage helpers in storage.ts (appendEpisode, loadEpisodes, episodeCount, loadSemantic, saveSemantic, loadWorking, saveWorking) bypass the native addon and use the backend. Native is still used for pure compute (jaccardSimilarity, searchEntries).

Gotcha: FileBackend.write() uses WAL: serialize to JSON → write to {key}.wal → fsync → rename to {key}. On startup, orphaned .wal files are replayed. This guarantees no half-written JSON even on process kill.

Gotcha: Episodic storage format changed from JSONL (episodes.jsonl, one JSON per line) to JSON array (episodes.json). Migration v1_to_v2 handles this automatically on startup. Also update any hardcoded episodes.jsonl paths in tests, docs, and dependent packages.

Schema migrations (Phase 1c)

Memory directories carry a schema-version.json file tracking the on-disk format version. On construction, MemoryEngine runs a MigrationEngine that detects the current version, runs any pending migrations sequentially, and updates the version file. Migrations live in packages/memory-core/src/migrations/:

Migration From To What it does
v1_to_v2 v1 (JSONL) v2 (JSON array) Converts episodes.jsonlepisodes.json, merges existing data, cleans .wal files
v2_to_v3 v2 (no _meta) v3 (with _meta) Adds _meta block (schemaVersion, layerName, createdAt, migratedAt) to all forge state files

To add a new migration:

  1. Create vN_to_v{N+1}.ts exporting a Migration object
  2. Add it to ALL_MIGRATIONS in migrations/index.ts
  3. Bump CURRENT_SCHEMA_VERSION in migrations/types.ts
  4. Write tests in migrations/migrations.test.ts

Gotcha: Migrations run through the StorageBackend interface, not fs directly. The v1_to_v2 migration uses fs because old JSONL files are outside the backend abstraction. All subsequent migrations should use backend.read/write.

Style & conventions

  • ESM in timps-code; CJS elsewhere.
  • No pre-commit hooks, no .cursor/rules/, no CLAUDE.md.
  • Changesets in .changeset/. Versioned packages: timps-code, timps-mcp, timps-vscode, packages/server.
  • Don't commit: dist/, out/, node_modules/, target/, .env, *.vsix, .timps/.
  • IDs: crypto.randomBytes(3).toString('hex') (not Math.random()).

Don't change without discussion

  • timps-mcp tool names (downstream configs depend on them).
  • VS Code extension activation events.
  • Memory on-disk schema (backwards compat).
  • Public CLI flags, slash commands, env vars.
  • The 25-tool count and their MemoryEngine lazy getter names.
  • StorageBackend interface methods (backends must stay compatible).
  • Episodic storage format (episodes.json JSON array — not JSONL).

PR checklist

  • npx tsc --noEmit passes in affected package (--max-old-space-size=4096 for timps-mcp).
  • Tests pass: npm test (root vitest run).
  • Coverage passes: npm run test:coverage meets 80% threshold.
  • Benchmark passes: npx tsx benchmark/index.ts --quick → 25/25 tools green, R@5 ≥ 90%.
  • grep -c "Math.random" benchmark/ returns 0.
  • If you added a tool, updated ALL_TOOLS and added smoke test to benchmark/index.ts.
  • If you changed memory on-disk format, add migration in timps-code/src/migrations/.
  • Changeset added for timps-code, timps-mcp, timps-vscode, or packages/server.
  • Updated AGENTS.md if public API changed.
  • If you added a new StorageBackend, implement all interface methods including beginTxn(), exists(), append(), and query().
  • If you added a forge layer, ensure it accepts backend?: StorageBackend as a constructor param after dir and uses this._backend.read/write when available.

Gotchas

  • npm run build builds all packages (mobile/plugins/docs may need extra tooling). Use build:ci in CI.
  • packages/server package.json version (2.0.4) doesn't match npm (2.0.0). Don't assume alignment.
  • Rust crates in crates/ are parallel re-implementation, not usable from TS.
  • MemoryEngine.contradiction.check(statement, autoStore?) defaults autoStore=true — pass false in tests.
  • ContradictionDetector requires >50% Jaccard vocabulary overlap to trigger. Exact phrasings needed for CONTRADICTION.
  • Unified test runner: Vitest across all packages. Run npm test (root) or vitest run for all tests. Coverage: npm run test:coverage (80% threshold enforced).
  • packages/server is missing @timps/memory-core in its package.json dependencies — it's installed by hoisting but won't resolve in npm ci --production.
  • timps-desktop has a JSX parsing issue in global.d.ts (missing space before declare keyword after a JSX block). If npx tsc --noEmit fails for timps-desktop, check .d.ts files for similar syntax issues.
  • test-coverage.yml matrix includes config, integration-base packages that no longer exist. Drop them from the matrix if they're removed from the workspace.
  • supply-chain-audit.yml uses google/osv-scanner-action@v1.0.2 (old). Update to v2.3.8 if the action fails.
  • packages/server check script is tsc but TypeScript isn't installed as a devDependency — it's hoisted from the root. CI may fail on strict isolated environments unless typescript is added to its devDependencies.

Phase 2c — Horizontal Scaling (June 2026)

New files in packages/memory-core/:

File Purpose
src/events/EventBus.ts Redis Pub/Sub with 10 typed channels, auto-skip own messages, async subscribe/unsubscribe
src/cache/CacheManager.ts Redis-backed cache with TTL, get-or-compute wrap(), pattern scan invalidation
src/backends/QdrantBackend.ts Vector store backend — upsertVector/searchVectors for embedding similarity search
docker-compose.yml Full stack: Postgres primary + 2 real streaming replicas + PgBouncer + Redis + Qdrant + N MemoryServers
Dockerfile Minimal node:20-alpine image, ESM dist + proto files
deploy/pgbouncer/pgbouncer.ini PgBouncer config (transaction pooling, 200 clients, 50 pool)
deploy/k8s/timps-memory.yaml K8s Deployment + HPA (2-10 pods, CPU 70%) + readiness probe
deploy/k8s/kustomization.yaml Kustomize overlay
src/chaos.test.ts 7 resilience tests: stateless recovery, shared backend, concurrent writes, graceful degradation

Public API additions

  • MemoryEngineOptions.cacheManager?: CacheManager — Redis cache for forge state (get-or-compute with TTL)
  • MemoryEngineOptions.eventBus?: EventBus — Redis Pub/Sub for cross-server events
  • MemoryEngine.cacheManager / MemoryEngine.eventBus — getters
  • store() publishes memory:stored event, recall() publishes memory:recalled (for queries >10 chars), consolidate() publishes memory:consolidated
  • MemoryServerOptions.eventBus?: { url? } | false — enables EventBus in server, auto-injects into engine
  • MemoryServerOptions.serverId?: string — server identity for event bus message dedup
  • GET /health/readiness — probes Postgres, Redis, EventBus, Cache, returns 503 if any fail
  • PostgresBackend now takes primary (writes) + optional replicas[] (reads, round-robin)
  • PostgresBackend.health() — ping primary + first replica
  • Exports: EventBus, CacheManager, QdrantBackend (via backends index)

Gotchas

  • PostgresBackend constructor changed: was connectionString, now primary + optional replicas[].
  • EventBus requires ioredis at runtime (lazy require). CacheManager also requires ioredis.
  • QdrantBackend requires @qdrant/js-client-rest at runtime (lazy require).
  • MemoryServer forwards event bus messages to WebSocket clients via wsServer.broadcast().
  • The memory:stored event payload includes id, content (truncated 200), type, tags, confidence, actorId.
  • The memory:recalled event only fires for queries >10 chars to reduce noise.
  • Scale out: docker compose up -d --scale memory=3 spins 3 server instances.
  • Secrets: compose uses ${POSTGRES_PASSWORD:-...}/${REDIS_PASSWORD:-...}/${GRAFANA_ADMIN_*:-...} env substitution — override with a .env file (echo "POSTGRES_PASSWORD=$(openssl rand -base64 18)" > .env). DB/Redis/Qdrant ports are NOT published to the host (internal network only); Redis runs with --requirepass; Grafana anonymous access defaults to false.
  • Streaming replicas (M42): postgres-replica-1/2 are TRUE streaming replicas, not empty standalones. Primary boot runs deploy/postgres/init-replication.sh (creates replicator REPLICATION LOGIN role + appends host replication all 0.0.0.0/0 scram-sha-256 to pg_hba.conf + reload — the stock postgres:16-alpine image ships NO TCP replication rule, so a hand-written primary_conninfo command would fail with no pg_hba.conf entry for replication). Replicas override the entrypoint with deploy/postgres/replica-entrypoint.sh: on empty data dir they ensure a physical slot on the primary and pg_basebackup -X stream -R, then append primary_conninfo (with password + application_name) and a standalone primary_slot_name GUC to postgresql.auto.conf. Gotchas: (1) primary_slot_name is a server parameter, NOT a libpq conninfo option — putting it inside primary_conninfo errors with invalid connection string syntax; (2) replica healthcheck is pg_isready -U timps -d timps_memory && psql ... -c "SELECT pg_is_in_recovery()" | grep -q t — it waits for the standby to actually be in recovery, and psql needs -d timps_memory (defaulting to the username gives database "timps" does not exist); (3) replicas self-provision their slots so WAL is retained across brief disconnects; (4) on the FIRST boot the compose volumes must be fresh (docker compose down -v) or the entrypoint skips pg_basebackup (PG_VERSION present) and starts a non-replicating instance.
  • The 6s StreamContext polling timer remains — Phase 2c replaces it with reactive forge-layer event pushes. This is marked as the gap before Phase 2d.

Phase 2d — Real-Time Sync (June 2026)

New/updated files in packages/memory-core/:

File Purpose
src/crdt/MemoryCRDT.ts LWW-Register-MV CRDT: incrementClock, mergeClocks, compareClocks, mergeEntries
src/server/ProjectRoom.ts Project-scoped agent room: join/leave/broadcast, auto Redis Pub/Sub on room:{projectId}:events
src/types.ts New fields on MemoryEntry: vectorClock, actorId, crdtStatus, conflicts, mergedFrom. New types: VectorClock, CrdtStatus, ConflictEvent, ConflictResolutionAction, ConflictResolutionRequest

Updated files in packages/memory-core/:

File What changed
src/events/EventBus.ts Added subscribeRaw/publishRaw/unsubscribeRaw for dynamic room: prefixed channels; widened EventBusChannel type
src/intelligence/contradiction.ts Added checkBeforeStore(newEntry, existingEntries) — synchronous Jaccard-based check against semantic entries
src/MemoryEngine.ts Added getSemanticEntries(), saveSemanticEntries(), checkBeforeStore(content) convenience methods
src/server/routes.ts Store handler runs sync conflict check before write; returns 409 Conflict on hit. New endpoints: GET /conflicts, GET /conflicts/:id, POST /resolve-conflict, POST /cancel-conflict
src/server/grpc.ts Store handler runs sync conflict check. New RPCs: ResolveConflict, CancelConflict, ListConflicts. AgentStream pushes ConflictEvent to affected agents
src/server/MemoryServer.ts ProjectRoom lifecycle: getOrCreateRoom, joinProjectRoom, leaveProjectRoom. REST endpoints: POST /room/join, POST /room/leave, GET /room/:projectId/agents
src/server/websocket.ts New WsEvent variants: conflict_detected, conflict_resolved, agent_joined, agent_left, project_event
src/client/MemoryClient.ts New methods: resolveConflict, cancelConflict, listConflicts, joinRoom, leaveRoom, getRoomAgents
src/client/grpc.ts New RPC wrappers: resolveConflict, cancelConflict, listConflicts
src/index.ts Exports new CRDT functions and ProjectRoom class
proto/timps/memory/v1/memory.proto Added project_id to StoreRequest, conflict_id/message to StoreResponse. New messages: ResolveConflictRequest/Response, CancelConflictRequest/Response, ConflictInfo, ListConflictsResponse, ProjectEvent. Added project_event to AgentStreamMessage oneof. New RPCs: ResolveConflict, CancelConflict, ListConflicts

Updated file in timps-code/:

File What changed
src/memory/memoryCoordinator.ts SSE stub replaced with thin backward-compat adapter. SSE server removed — use gRPC + WebSocket instead. Leases/conflict queue delegated to memory-core CRDT infrastructure.

Gotchas

  • MemoryEngine.getSemanticEntries() returns the raw semantic entries array (not a copy). Mutation is safe since loadSemantic re-reads from disk.
  • Synchronous conflict detection runs at write time in both REST and gRPC Store handlers. If a conflict is found, the store is aborted and a 409 Conflict response is returned with the ConflictEvent payload.
  • To bypass conflict detection (e.g., for internal writes), use (engine as any).contradiction?.checkBeforeStore(...) is called from routes/grpc — internal engine methods (engine.store()) do NOT run conflict detection.
  • EventBus.subscribeRaw returns an unsubscribe function. ProjectRoom stores this for cleanup on destroy().
  • gRPC AgentStream now pushes ConflictEvent memory insights to agents when they check_conflicts or emit stored_memory events.
  • ProjectRoom auto-subscribes to room:{projectId}:events on Redis. When agentCount drops to 0, destroy() is called and the subscription is released.
  • The memoryCoordinator.ts SSE stub in timps-code is now a shell — all real-time coordination goes through the MemoryServer gRPC/WebSocket endpoints.

Phase 2e — Project-Scoped Isolation at Scale (June 2026)

New types and utilities:

File What changed
src/types.ts Added OrgScope = { orgId: string; teamId?: string; projectId: string }
src/backends/types.ts Added OrgScope re-export, buildKey(), scopeListPrefix() — stable key derivation memory:{orgId}:{teamId}:{projectId}:{key}
src/rateLimiter.ts RateLimiter class: Redis-backed with in-memory fallback, per-org sliding window counters with Lua scripts

Backend changes (all backends):

  • StorageBackend interface — new methods setScope(scope), getScope(). All read/write/delete/list/exists/append accept optional scope param that overrides active scope.
  • InMemoryBackend — scope-aware Map storage, _activeScope state, _resolveScope() picks active scope or explicit param, keys prefixed with memory:{org}:{team}:{project}:{key}.
  • PostgresBackendsetScope manages session-level org_id/team_id/project_id via SET SESSION variables; RLS policies on mem_store table; buildKey generates scoped keys for non-RLS tables.
  • RedisBackendsetScope sets _activeScope, all keys prefixed with scope prefix for logical database partitioning.
  • QdrantBackendsetScope sets _activeScope, upsert/search inject org_id payload filter to enforce tenant isolation. Renamed internal _generateId_generateUuid for clarity.
  • FileBackend — unchanged (no scope support; scope-aware backends are the future).

MemoryEngine changes:

  • Accepts orgScope in MemoryEngineOptions.
  • Constructor calls backend.setScope(orgScope) and passes this._backend to all storage functions (loadSemantic(dir, backend), saveSemantic(dir, data, backend), etc.) — fixing the previous gap where storage.ts functions bypassed the engine's backend.
  • New method multiProjectRecall(query, projectIds, options?) — iterates project scopes, temporarily switches backend scope, calls recall(), restores original scope. Falls back to single-project recall when no orgScope set.
  • New static method deriveProjectId(remoteUrl, branch?) — stable 12-char hex hash from git remote + branch for cross-machine project ID consistency.
  • New static method extractOrgScope(req) — reads x-org-id, x-team-id, x-project-id headers from request-like objects.
  • New getter backend — exposes the underlying StorageBackend.
  • store() enriches stored entries with org:, team:, project: tags from orgScope.

Auth middleware (src/server/auth.ts):

  • requireOrgScope middleware reads x-org-id/x-team-id/x-project-id headers and attaches them to the request.
  • Token/API key auth can now include orgClaim, teamClaim, projectClaim.
  • requireAuth exported alongside authenticateRequest.

MemoryServer changes (src/server/MemoryServer.ts):

  • Rate limiter middleware (rateLimiter.check(orgId, endpoint)) on all write endpoints.
  • GET /health/readiness probes Postgres, Redis, EventBus, Cache, RateLimiter.
  • Server creates RateLimiter instance, injects into route handlers.

Migration v3→v4 (src/migrations/v3_to_v4.ts):

  • Scans all backend keys, skips DATA_FILES (episodes.json, semantic.json, working.json) and meta files.
  • Writes .org-scope.json sidecar with defaultScope: { orgId: "default", projectId: dirNameHash }.
  • Adds orgScope to _meta blocks of layer state files.
  • Added to ALL_MIGRATIONS, CURRENT_SCHEMA_VERSION bumped to 4.

New exports (src/index.ts):

  • RateLimiter, OrgScope type, buildKey, scopeListPrefix, deriveProjectId.

storage.ts changes

All storage functions now accept an optional backend?: StorageBackend parameter:

  • loadWorking(dir, backend?)
  • saveWorking(dir, state, backend?)
  • appendEpisode(dir, episode, backend?)
  • loadEpisodes(dir, count, backend?)
  • episodeCount(dir, backend?)
  • loadSemantic(dir, backend?)
  • saveSemantic(dir, entries, backend?)

When backend is omitted, falls back to getBackend(dir) (legacy FileBackend). MemoryEngine always passes this._backend.

Gotchas

  • Arg order: saveSemantic(dir, entries, backend?) — backend is the 3rd param. appendEpisode(dir, episode, backend?) — backend is the 3rd param. loadSemantic(dir, backend?) — backend is the 2nd param. Double-check arg order when calling from MemoryEngine.
  • Jaccard dedup triggers on short strings: Content like 'A: pattern 1' vs 'A: pattern 2' have Jaccard similarity >0.8, triggering dedup. Use sufficiently distinct content strings in tests.
  • Multi-project recall requires shared backend: multiProjectRecall works by temporarily switching scope on the engine's backend. Engines with separate backends cannot cross-project recall. Use a single InMemoryBackend shared across scope-managed engines in tests.
  • Pre-existing test failure: ContextVector — L19 > match returns empty when no similar contexts fails because time/day matching (TimeDiff < 60min, same dayOfWeek) always triggers on captures/matches within the same second. Unrelated to Phase 2e.
  • StorageBackend.setScope() is one-way: Once set, all subsequent ops use that scope until changed. multiProjectRecall restores the original scope after each project iteration.
  • Migration v3→v4 does NOT wrap data files: episodes.json, semantic.json, working.json are raw arrays. They get a .org-scope.json sidecar instead of being wrapped with _meta. Layer state files (with _meta) get inline orgScope metadata.
  • CURRENT_SCHEMA_VERSION is now 4. Bump for any on-disk format change.

Phase 3a — Skill/Plugin Marketplace (WASM Sandbox + Registry + Static Analysis)

New files in packages/memory-core/:

File Purpose
src/marketplace/types.ts Plugin manifest, permissions, dependencies, submission, analytics types
src/marketplace/scanner.ts Static analysis pipeline — pattern scanning, permission validation, npm audit, checksum verification
src/marketplace/registry.ts PluginRegistry — CRUD, submit, search, rate/review, analytics tracking, all backed by StorageBackend
src/marketplace/resolver.ts Dependency resolver — semver constraint matching, version conflict detection
src/sandbox/WasmSandbox.ts WasmSandbox — install/uninstall WASM plugins, execute via wasmtime or JS proxy with ABI permission enforcement
src/server/marketplaceRoutes.ts Express router — POST /marketplace/submit, GET /marketplace/plugins, GET /marketplace/plugins/:name, POST /plugins/:name/rate, GET /plugins/:name/reviews, POST /marketplace/events
src/marketplace.test.ts 14 tests: scanner (clean code, eval rejection, undeclared perms, size limit, checksum), registry (submit/approve/reject, list, search, downloads, ratings), resolver (simple, conflict, empty)

Updated files:

File What changed
src/index.ts Exports PluginRegistry, runStaticAnalysis, verifyChecksum, approved, resolveDependencies, WasmSandbox, createMarketplaceRoutes, all marketplace types
src/server/MemoryServer.ts Mounts createMarketplaceRoutes at /marketplace
packages/plugin-sdk/src/types.ts Added Permission type, timps field to PluginManifest (version, permissions, dependencies)
timps-code/src/commands/plugin.ts pluginInstall now resolves marketplace plugins (fetches from /marketplace/plugins/:name API, resolves dependencies), pluginList shows [marketplace]/[npm] tags + permissions
apps/marketplace/src/components/PluginGrid.tsx Fetches plugins from live /marketplace/plugins API (falls back to empty on error)
apps/marketplace/src/components/PluginCard.tsx Shows rating + download count from API data

Architecture

Plugin Author                         TIMPS User
       │                                     │
       │  1. POST /marketplace/submit         │
       │  ──→ PluginRegistry.submit() ──→     │
       │     │                                │
       │     ├─ 2. Static analysis             │
       │     │   (scanner.ts)                 │
       │     │   - pattern scanning            │
       │     │   - permission validation       │
       │     │   - npm audit                   │
       │     │   - checksum verification       │
       │     │   - package size check          │
       │     │                                │
       │     ├─ 3. Auto-approved or queued     │  4. Browse marketplace
       │     │                                │  ←── GET /marketplace/plugins
       │     │                                │  5. Install: timps install <name>
       │     │                                │  ──→ Fetches plugin info, resolves deps
       │     │                                │  6. Plugin runs in WasmSandbox
       │     │                                │     (permission enforcement via ABI proxy)
       │     │                                │
       │     │  7. Usage telemetry ──→         │
       │     │   POST /marketplace/events      │
       │     │                                │
       │     └─ 8. Ratings/reviews ──→         │
       │        POST /plugins/:name/rate      │

Key design decisions

  • Scanner rejects on eval, Function(), child_process, undeclared network/fs access, oversized packages — not just warnings
  • Dependency resolver uses semver matching^ and ~ constraints, version conflict detection
  • WasmSandbox uses subprocess WASM executionwasmtime CLI for native WASM, JS proxy fallback for Node.js
  • Marketplace API is a flat Express router mounted alongside memory routes in MemoryServer
  • PluginRegistry stores everything via StorageBackend — works with InMemoryBackend (tests), PostgresBackend (production), RedisBackend (caching)
  • CLI resolves dependencies at install time — marketplace API returns full plugin info with dependency graph

Gotchas

  • Scanner requires a base64-decoded payloadrunStaticAnalysis(payload, manifest) takes base64-encoded plugin content
  • PluginRegistry.submit() rejects on checksum mismatch before any analysis — saves CPU on invalid submissions
  • WasmSandbox.executeJS() creates a temporary script per execution — permissions are baked into the script via __permissions const
  • Marketplace frontend uses NEXT_PUBLIC_MARKETPLACE_API env var — defaults to localhost:4100, configure in production
  • CLI's pluginInstall auto-detects marketplace plugins — if the name has no npm scope prefix (@), it tries the marketplace first
  • Dependency resolver has no package registry fallback — it only resolves from the available map passed in. For production, wire to the PluginRegistry.
  • All 14 marketplace tests must pass before push — run npx vitest run packages/memory-core/src/marketplace.test.ts
  • Pre-existing test failure: ContextVector — L19 still fails (unrelated to Phase 3a)

Phase 3b — Observability & Telemetry (Privacy-Preserving)

New files in packages/memory-core/:

File Purpose
src/telemetry/types.ts Telemetry level (off/local/anonymous), config, span, metric, histogram, anonymous payload types
src/telemetry/MetricsRegistry.ts Counter, histogram, gauge storage with Prometheus text export and percentile computation
src/telemetry/TracerProvider.ts Lightweight tracer with span lifecycle, no-op fallback, SpanHandle for safe usage
src/telemetry/RedactionPipeline.ts Privacy redaction — strips all content/identifiers, preserves only safe structural attributes
src/telemetry/TelemetryManager.ts Central config — off (zero alloc), local (in-memory + /metrics), anonymous (+ redacted hourly export)
src/telemetry/instrumentation.ts Proxy-based wrappers for IMemoryLayer (9 methods) and StorageBackend (read/write/delete/list/exists/append); CRDT conflict recording
src/telemetry/telemetry.test.ts 16 tests covering metrics, spans, redaction, telemetry manager, layer/backend/CRDT instrumentation
src/server/telemetryRoutes.ts Express router: GET /metrics (Prometheus), GET /metrics/json, POST /metrics/reset
deploy/prometheus/prometheus.yml Prometheus scrape config — memory-server via dns_sd_configs (per-replica series with --scale memory=N) + otel-collector Prometheus exporter on :8888
deploy/otel/otel-collector.yml OTel Collector config: OTLP receiver → batch processor → Prometheus exporter + debug
deploy/grafana/dashboards.yml Grafana dashboard provisioning config (auto-loads from /var/lib/grafana/dashboards)
deploy/grafana/dashboard-memory-health.json Panel: stores by layer, contradiction rate, semantic-entries growth
deploy/grafana/dashboard-performance.json Panel: recall latency p50/p95/p99, store latency, backend breakdown, ops/sec
deploy/grafana/dashboard-agent-activity.json Panel: CRDT conflicts + checks, consolidation merge latency
deploy/grafana/dashboard-system.json Panel: backend ops/latency, error rate, guard rejections, throughput, semantic entries (no external exporters)

Updated files:

File What changed
src/MemoryEngine.ts Added telemetry?: TelemetryConfig to MemoryEngineOptions; initializes TelemetryManager, instruments backend on construction; wraps 4 IMemoryLayer forges (Chronos, Echo, Harmonic, Aether) via Proxy; adds spans + metrics to store(), recall(), consolidate(); adds telemetry getter
src/server/MemoryServer.ts Added telemetry?: TelemetryConfig to MemoryServerOptions; creates TelemetryManager and injects into engine; mounts /metrics from telemetryRoutes; adds telemetry health check to /health/readiness; adds telemetryManager getter
src/index.ts Exports TelemetryManager, MetricsRegistry, Tracer, NoopTracer, RedactionPipeline, instrumentLayer, instrumentBackend, instrumentCRDT + all types
docker-compose.yml Added prometheus, grafana, otel-collector services; added TIMPS_TELEMETRY_LEVEL and TIMPS_TELEMETRY_OTEL_ENDPOINT env vars to memory service; added volumes for prometheus, grafana

Telemetry Architecture

MemoryServer (instrumented)
  │
  ├─ /metrics (Prometheus text) ←─ Prometheus (scrape)
  ├─ POST /metrics/reset
  └─ OTLP exporter (optional) → OTel Collector → Jaeger/Grafana Tempo

Three Modes

Level Metrics Traces Export Privacy
off None (no-op tracer, zero alloc) None None N/A
local In-memory counter/histogram/gauge In-memory spans (ring buffer, capped 10k) Prometheus scrape at /metrics All data stays on server
anonymous Same as local Same as local Hourly redacted export (aggregates only) Redaction pipeline strips content + identifiers

Redaction Guarantee

The RedactionPipeline enforces privacy at the attribute level. Safe keys preserved: timps.layer, timps.version, db.system, db.operation, error.type, backend.type, cache.hit, plugin.name, resolution, exception.*, http.*, net.*. Stripped keys: content, query, query.text, org_id, project_id, actor_id, file.path, user.*, agent.*, entry.id, any custom keys in extraRedactKeys.

Grafana Dashboards

4 pre-built dashboards at deploy/grafana/dashboard-*.json (all queries reference only metrics the server actually emits — verified, no dashboard-eval.json):

  1. Memory Health — stores by layer (stacked), contradiction rate, semantic-entries growth
  2. Performance — recall/store latency p50/p95/p99, backend breakdown, ops/sec
  3. Multi-Agent Activity — conflict detections + conflict checks, consolidation merge latency
  4. System — backend ops/latency, error rate, guard rejections, store/recall/consolidate throughput, semantic entries (no external exporters required)

Architecture Notes

  • No OTel SDK dependency. Telemetry is pure TypeScript with zero runtime dependencies. OTel Collector receives metrics via Prometheus scrape, not OTLP push (by default). For full OTLP tracing, add @opentelemetry/sdk-node as an optional peer dependency.
  • Proxy-based instrumentation. instrumentLayer() uses Proxy to intercept IMemoryLayer methods without modifying forge classes. Non-IMemoryLayer methods pass through transparently.
  • Anonymous export runs hourly. The TelemetryManager sets a setInterval that calls onAnonymousExport with the redacted payload. Wire this to an HTTP endpoint or file sink in production.
  • Prometheus /metrics endpoint is mounted at the MemoryServer when telemetry level is local or anonymous. Prometheus discovers every memory replica individually via dns_sd_configs (A-record query on the compose service name, port 4100) — no round-robin interleaving when scaled with --scale memory=N. Prometheus also scrapes the OTel Collector's Prometheus exporter on otel-collector:8888, which re-exports anything pushed via OTLP (docker-compose exposes 8888:8888).

Gotchas

  • Anonymous export is opt-in only. Setting level: 'anonymous' enables hourly redacted export. By default (level: 'off'), zero telemetry is collected.
  • Telemetry config must be set before MemoryEngine construction. The telemetry field in MemoryEngineOptions is read during construction to wrap the backend and forge layers.
  • Proxy wrapping is shallow. Only the 9 IMemoryLayer methods (store, retrieve, verify, contradict, archive, getProvenance, explain, audit, decay) are instrumented. Forge-specific methods like weave(), foresight(), predict() are not wrapped — they appear on the forge classes but not on IMemoryLayer. Instrument them directly in MemoryEngine's store/recall methods instead.
  • instrumentBackend() preserves sync/async semantics. wrapAsync detects whether the underlying method returns a Promise: sync backends (FileBackend, InMemoryBackend, SQLiteBackend) stay sync, async backends (PostgresBackend, RedisBackend) stay async. This is what makes telemetry usable at all — the previous version forced every method async, which broke MemoryEngine's sync storage layer (loadSemantic/saveSemantic) and crashed at startup on pending migrations whenever telemetry was enabled.
  • CRDT metrics are wired to real production events (M39 fix) — not just registered for tests:
    • crdt.check — incremented once per store() write
    • crdt.conflict.detected{resolution}dedup (Jaccard dedup in store), chronos (ChronosForge conflict), guard_rejected (L15 guard), write_conflict (REST/gRPC 409)
    • crdt.merge.latency — histogram recorded per consolidate() run
    • Accessible via engine.crdtMetrics (no-ops when telemetry off).
  • Storage gauge: memory.semantic_entries is emitted on every store() (no memory.storage_size_bytes metric exists — the dashboards use timps_memory_semantic_entries).
  • No eval dashboard. Eval runs (timps eval:run) are batch CLI processes that Prometheus never scrapes; there is deliberately no dashboard-eval.json in deploy/grafana/ (docker-compose mounts only the four real dashboards).
  • Histogram buckets are fixed: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] ms. Tune these for your latency range.
  • CRDT metrics are only collected when instrumentCRDT() is called with an active telemetry manager. The CRDT module itself is not modified.
  • Pre-existing test failure: ContextVector — L19 still fails (unrelated to Phase 3b). chaos.test.ts also has 2 pre-existing failures (cross-instance test, concurrent-write-* recall counts) from the documented short-string Jaccard dedup gotcha.

Phase 3d+3e — Secure Config, Rate Limits, Auth, Billing Removal (June 2026)

Phase 3d — Secure LLM Key Vault + Rate Limiter + User Store

New files in timps-code/:

File Purpose
src/config/keyVault.ts AES-256-GCM encrypt/decrypt for LLM API keys at rest. Output format: hexIV:hexTag:hexCiphertext. isEncrypted() regex /^[0-9a-f]{32}:[0-9a-f]{32}:[0-9a-f]+$/
src/services/providerRateLimiter.ts Per-provider sliding-window rate limiter: daily cap + min delay between requests. Persists usage to .timps/rate-limits/. Resets at midnight UTC.
src/services/userStore.ts Local user store with scrypt password hashing, session tokens (24h expiry), stored in ~/.timps/users.json. Tokens validated via crypto.timingSafeEqual.

Updated files in timps-code/:

File What changed
src/config/types.ts Added TimpsConfig fields: providerLimits, rateLimitStrategy, fallbackChain
src/config/config.ts Auto-encrypts API keys on save, auto-decrypts on load; avoids double-encryption with isEncrypted() check
src/models/providerMesh.ts streamWithFallback() enhanced with rate limit checks before each provider call; reads providerLimits from config

CLI commands (all in src/commands/executor.ts):

Command Action
/config:encrypt-key <key> Encrypt an API key and store in config
/config:set Set config values (provider, model, etc.)
/config:show Display current config (keys masked)
/config:set-provider Set provider, model, and API key
/config:provider-config List/configure provider configs
/config:delete-key Remove a key from config
/auth:login <username> <password> Login with password, get session token
/auth:status Show current login status
/auth:register <username> <password> Register new user
/auth:logout Clear session token
/auth:reset-password <username> <old> <new> Change password
/limits:show Show provider usage limits

Phase 3e — Eval Bug Fixes + Bridge Stub Deletion

Bug fixes:

  • packages/memory-core/src/eval/storage.tsInMemoryBackend.read() returns already-parsed object; removed extraneous JSON.parse() on the return value that caused double-parse errors.
  • packages/memory-core/src/eval/regression.tsRegressionDetector.check() when baselineValue === undefined must still compare metric.value against threshold. Previously a missing baseline file made the gate pass everything.
  • timps-code/src/commands/executor.tseval:run handler: await async evaluateDataset() call (was returning Promise to CLI instead of EvalResult). Regression summary formatting: seenBaselines Map type narrowed from Map<string, any> to Map<string, MetricInfo>.

Bridge stub deletion:

  • Deleted timps-code/src/services/bridge.ts (85 lines) — empty stubs for bridge.cloud.sync(), bridge.monitor.getStatus(), bridge.plugins.getAnalytics(), bridge.billing.getSavedAmount().
  • Deleted timps-code/src/services/__tests__/bridge.test.ts — tests for deleted stubs.

Key design decisions

  • All billing/SaaS stubs removed — TIMPS is always 100% free, self-hosted.
  • API keys encrypted at rest with AES-256-GCM; IV+tag+ciphertext concatenated with : delimiters.
  • Rate limits protect user's own LLM budget, not a tiered-pricing structure.
  • Fallback chain in providerMesh handles 429/5xx by trying next provider in chain.
  • Auth is simple local username/password — no enterprise SSO, no OAuth.
  • Session tokens expire after 24h with no refresh mechanism (re-login required).

Gotchas

  • keyVault.encrypt() output format is hexIV:hexTag:hexCiphertext. isEncrypted() tests with regex /^[0-9a-f]{32}:[0-9a-f]{32}:[0-9a-f]+$/.
  • providerRateLimiter stores usage in .timps/rate-limits/ as JSON files, one per day per provider.
  • User sessions expire after 24 hours; tokens stored in ~/.timps/auth-token.json.
  • Rate limits are checked before each provider call in streamWithFallback() — a blocked provider triggers fallback to next in chain.
  • The /config:encrypt-key handler changes the old plaintext key to the encrypted version in-memory, then saves; it does NOT add a separate encryption step on load since config.ts auto-decrypts.

Phase 4a — Vector Search at Scale (June 2026)

recall() is now async — all callers must await it.

Architecture

store() ──→ sync write (BM25 index) ──→ async EmbeddingQueue ──→ Qdrant (dense + sparse vectors)
                                                                       │
recall() ──→ Stage 1a: BM25 MiniSearch (always, fast-path for <1K) ────┤
             Stage 1b: Qdrant hybridSearch (dense + sparse, >1K) ──────┤
             Stage 1c: KG expansion (shared-tag overlap) ──────────────┤
             Stage 1d: RRF fusion (k=60) ←─────────────────────────────┘
             Stages 2-7: ProvenanceForge, ConfidenceCalibrator, FalseMemoryDetector, ContextVector, RehearsalEngine

New files in packages/memory-core/src/:

File Purpose
embedding/types.ts EmbeddingConfig, EmbeddingResult, QueueItem, EmbeddingStatus types
embedding/EmbeddingService.ts Provider-agnostic embedding via Ollama (nomic-embed-text, 384d) or OpenAI (text-embedding-3-small, 768d). Batched API calls, graceful fallback to zero-vectors.
embedding/EmbeddingQueue.ts Async queue with configurable batch size (16) and flush interval (500ms). In-memory queue + StorageBackend crash recovery. Background worker auto-drains on flush.
embedding/index.ts Re-exports
search/rrf.ts Reciprocal Rank Fusion: rrfFuse(lists, k=60), rrfFuseWithNames(namedLists, k=60). Scores computed as sum(1/(k + rank)). Results deduplicated by content.
search/hybridRetriever.ts hybridRecall(entries, query, options?, qdrantBackend?) — orchestrates BM25 + Qdrant + KG → RRF fusion. Returns top-50 candidates. Graceful degradation on Qdrant failure.

Updated files in packages/memory-core/src/:

File What changed
backends/QdrantBackend.ts HNSW config: m:32, ef_construct:128, indexing_threshold:50000, on_disk. Sparse vector support (bm25 named sparse). New methods: hybridSearch(), textToSparseVector() (TF-based with stopword filtering, hash to 0-65535), upsertWithSparseVector(), upsertVectorsWithSparse(), addEmbedding(), addEmbeddings()
types.ts SearchOptions extended: useHybrid, useMiniSearch flags
MemoryEngine.ts recall()async (breaking change). Adds qdrantBackend?: QdrantBackend + embedding?: EmbeddingConfig to MemoryEngineOptions. store() fires async embedding queue (fire-and-forget). recall() uses hybrid search when Qdrant configured & entries >1K. New methods: backfillEmbeddings(), dispose(), embeddingStatus getter.
index.ts Exports EmbeddingService, EmbeddingQueue, rrfFuse, rrfFuseWithNames, hybridRecall, QdrantBackend hybrid methods
server/routes.ts New endpoints: POST /embedding/backfill, GET /embedding/status

Updated files in timps-code/:

File What changed
src/memory/memory.ts Added backfillEmbeddings(), embeddingStatus, async searchFacts(). getContextString() → async
src/commands/executor.ts New handlers: /memory:embed-backfill, /memory:embed-status

Key design decisions

  • MiniSearch kept for <1K entries — Qdrant only delegated when entry count exceeds 1K, avoiding network overhead for small projects.
  • RRF with k=60 — standard hybrid search parameter; lists with different lengths weighted fairly by rank position.
  • Embedding queue backed by in-memory array + StorageBackend crash recovery — not Redis-only, so single-process deployments don't require Redis running.
  • Sparse vectors computed client-side — simple TF-based extraction with stopword filtering and hash-to-index mapping (0-65535), avoids external tokenizer dependency.
  • Knowledge graph expansion via shared-tag overlap — 2+ shared tags between a top-BM25 result and another memory creates a KG edge. No external graph DB needed.
  • Graceful degradation — if Qdrant returns error or embedding provider down, search falls back to BM25-only with zero data loss.
  • Async embedding is fire-and-forget — store() returns immediately without waiting for embedding. BM25 handles immediate recall queries. Embedding computed in background batch.
  • Embedding config per engine instance — passed via MemoryEngineOptions, not ambient from config file. Different engines can use different providers.

Gotchas

  • recall() is now async. All callers must use await. This includes MemoryEngine.getContextString(), multiProjectRecall(), Memory.searchFacts(), and all REST/gRPC handlers and tests.
  • Hybrid search pipeline: Stage 1a BM25 (always) → Stage 1b Qdrant hybrid (dense + sparse BM25 vectors) → Stage 1c KG expansion (shared-tag overlap) → Stage 1d RRF fusion. Stages 2-7 (ProvenanceForge, ConfidenceCalibrator, FalseMemoryDetector, ContextVector, RehearsalEngine) run on the fused results.
  • Qdrant HNSW params: m: 32, ef_construct: 128, indexing_threshold: 50000. Collection includes named sparse vector bm25 for native BM25-vector search.
  • Sparse vector format: { indices: number[], values: number[] } where indices are hash values (0-65535) and values are 1 + log2(TF).
  • Embedding queue: accumulates items for max 500ms, then sends batch of up to 16 to embedding provider. Returns zero-vectors on failure (graceful degradation).
  • Store-then-immediately-recall finds the memory via BM25 (sync) before embedding completes (async). The embedding arrives eventually in Qdrant.
  • Pre-existing eval import errors in timps-code/src/commands/executor.ts (seedEngineWithDataset, loadAllDatasets, etc. not exported from @timps/memory-core) are unrelated to Phase 4a.
  • MemoryEngine.test.ts and memory-unified.test.ts both test recall() synchronously with await. If a new test file calls recall() without await, the result will be a Promise, not an array — the test will silently pass with wrong assertions.

Phase 4b — Incremental Layer Computation (June 2026)

New files in packages/memory-core/src/computation/:

File Purpose
types.ts Task types (eigenmode, contradiction, decay_scores, materialized_view, full_recompute), ComputationTask, MaterializedView<T>, view entry types, ComputationHandlers
ComputationQueue.ts Background batch queue — in-memory + StorageBackend crash recovery (16-item batch, 500ms interval), generic string-dispatch to registered handlers
MaterializedViews.ts View get/set/isStale/refresh/delete by name. Pre-defined: contradictions (60s TTL), working_memory (30s TTL), velocity (120s TTL), drift (120s TTL). Backed by StorageBackend under views: prefix.
LSHIndex.ts Locality-sensitive hashing index — random projection (4 tables × 8 bits, embed dim 64). insert(id, content), delete(id), query(content, maxResults?) returns candidate IDs. No generic type parameter — stores string IDs keyed by string content.

Updated files:

File What changed
HarmonicSheafWeaver.ts weave() sets _dirtyEigenmodes flag + tracks _pendingNodeIds instead of clearing spectral cache. detectContradictions() and predict() use computeEigenpairsWarm() (8 iterations, seeded from cached eigenvectors) instead of computeSmallestEigenpairs() (40 iterations). Added computeEigenpairsWarm() function. Added isEigenmodeDirty getter and refreshEigenmodes().
AetherForgeERL.ts Same pattern as HSW: _dirtyEigenmodes, _pendingNodeIds, warm-started eigenpair computation, isEigenmodeDirty getter, refreshEigenmodes()
EchoForge.ts _decayScoreCache: Map<string, number> — cached effectiveEcho() results. _cachedEcho(nodeId, atMs) returns cached score or computes + caches. _invalidateDecayScore()/_invalidateAllDecayScores(). All effectiveEcho() call sites replaced with _cachedEcho(). Cache invalidated on verify(), contradict(), archive(), store() retrieval increment, and consolidate() changes. refreshDecayScores() for ComputationQueue worker.
intelligence/contradiction.ts LSHIndex field. Constructor rebuilds LSH from existing positions. check() queries LSH buckets for candidates (max 16 per claim) instead of O(N) scan. Falls back to full scan when LSH returns empty (cold start). store() inserts into LSH; delete() removes from LSH; 200-position cap also removes from LSH.
MemoryEngine.ts New fields: _computationQueue, _materializedViews. Constructor initializes both. _computationHandlers() registers eigenmode, contradiction, decay_scores, materialized_view handlers. store() enqueues 4 fire-and-forget tasks per write. dispose() calls _computationQueue.stop(). Exports computationQueue and materializedViews getters.
computation/types.ts Exports ViewEntry union type. ComputationHandlers changed to Record<string, (task) => Promise<void>>.
computation/MaterializedViews.ts Exports constant names CONTRADICTION_VIEW, WORKING_MEMORY_VIEW, VELOCITY_VIEW, DRIFT_VIEW. All internal method references use constant names.
computation/LSHIndex.ts Changed from generic LSHIndex<T extends { id: string; content: string }> to non-generic LSHIndex. insert(item: T)insert(id: string, content: string). query returns string[] of IDs. getAll() returns string[].

Key design decisions

  • ComputationQueue follows EmbeddingQueue pattern — in-memory array + StorageBackend crash recovery, not Redis-dependent.
  • All forge incremental updates are fire-and-forgetstore() enqueues tasks but returns immediately; BM25 handles immediate recall queries; incremental compute arrives eventually.
  • Warm-started eigenmode computation — cached eigenvectors seeded as initial guesses for power iteration (2-5 iterations vs 40 from scratch). Uses computeEigenpairsWarm() in both HSW and AetherForgeERL.
  • LSHIndex is non-generic — stores string IDs keyed by content. Simpler interface for contradiction detector integration.
  • Materialized views have per-view TTL — stale views trigger fresh computation on read. Views stored under views: prefix in the same StorageBackend.
  • Decay score cache is in-memory only — not persisted. Invalidated on any mutation (verify, contradict, archive, retrieval increment). refreshDecayScores() recomputes all scores on a periodic cycle.

Gotchas

  • ComputationQueue constructor arg order: new ComputationQueue(handlers, backend?, config?) — handlers first, then optional backend and config.
  • computeEigenpairsWarm() signature: (n, triples, k, cachedValues?, cachedVectors?, cachedN?, maxIter?). Uses cachedVectors[i * prevK + vec] interpolation for warm-start seeding. Falls back to deterministic seeding (Math.sin) when cache doesn't match dimension.
  • EchoForge _cachedEcho() returns 0 for unknown node IDs — not undefined. Consumers get a valid number.
  • LSH query candidates are limited to 16 per claim — tunable by maxResults parameter on query(). Falls back to full O(N) scan when LSH returns 0 candidates.
  • ContradictionDetector check() calls this._lsh.query(claim, 16) — returns string[] IDs. The autoStore=true default still stores each claim after checking.
  • MemoryEngine _computationHandlers() register per-task-type handlerseigenmode, contradiction, decay_scores, materialized_view. Worker routes task.type to the matching handler via this.handlers[task.type].
  • MaterializedViews.refresh(name, computeFn) requires a compute function — the materialized_view handler passes async () => [] as fallback. Override in production by registering a real compute handler at engine level.
  • Pre-existing test failure: ContextVector — L19 > match returns empty when no similar contexts fails (unrelated to Phase 4b). Time/day matching triggers on captures within the same second.

Phase 4c — Memory Compaction (June 2026)

3-tier compaction (classify → cluster → consolidate → compress → archive → delete) that reduces active storage by ~79% at scale while improving recall quality.

Architecture

Scheduler (6h / manual `timps compact`)
  │
  └─→ CompactionPipeline.run(entries)
        │
        ├─ Step 1: MemoryClassifier.classifyAll()
        │   └─ Assigns each entry → hot / warm / cold / deleted
        │      Based on age, recall frequency, importance, contradiction status, pin status
        │
        ├─ Step 2: ClusterEngine.cluster()
        │   ├─ With embeddings: k-means++ (cosine distance, sqrt(N/2) clusters)
        │   └─ Without embeddings: layer + first-tag fallback grouping
        │
        ├─ Step 3: LLMConsolidationEngine.consolidate()
        │   ├─ Sends cluster to user's LLM (BYOK — OpenAI/Ollama/Anthropic compatible)
        │   ├─ Constitutional system prompt with 10 guardrail rules
        │   ├─ ConstitutionalGuardrails post-processing:
        │   │   ├─ Fabrication check (keyword verification against source)
        │   │   ├─ Instruction leakage strip
        │   │   ├─ Contradiction preservation check
        │   │   └─ Confidence scoring (high/medium/low)
        │   └─ Falls back to rule-based concatenation when no LLM configured
        │
        ├─ Step 4: ContentCompressor.compress()
        │   ├─ Lossy: shorten verbose content, preserve first sentence + key entities
        │   └─ Lossless: embedding always kept, error messages preserved
        │
        ├─ Step 5: ArchiveBackend.archiveBatch()
        │   ├─ Cold memories → gzipped JSON files (archive_{ts}.json.gz)
        │   ├─ Index maintained for quick listing/restore
        │   └─ Not indexed in Qdrant — archive is cold storage
        │
        ├─ Step 6: Purge deleted originals
        │   └─ Entries marked 'deleted' (consolidated >30 days ago) removed from active store
        │
        └─ Step 7: Enqueue materialized view refresh + eigenmode recompute

New files in packages/memory-core/src/compaction/

File Purpose
types.ts CompactionConfig, ClassifiedMemory, ConsolidatedFact, CompressionResult, ArchiveManifest, CompactionReport, LLMConsolidationRequest/Response, GuardrailCheckResult
MemoryClassifier.ts Tier assignment: hot/warm/cold/deleted based on age, importance, recall count, contradiction status, pin status
ClusterEngine.ts k-means++ clustering on embedding vectors (cosine distance, random projection). Falls back to layer + tag grouping
LLMConsolidationEngine.ts OpenAI-compatible LLM summarization with constitutional guardrail prompt. BYOK — falls back to rule-based
ConstitutionalGuardrails.ts Post-processing: fabrication check, instruction leakage detection/strip, contradiction preservation, confidence scoring
ContentCompressor.ts Lossy compression: shortens verbose content while keeping embeddings. Preserves errors, extracts first sentence + key entities
ArchiveBackend.ts Cold storage: gzipped JSON archive files, index for listing/restore, batch operations
CompactionPipeline.ts Orchestrator: classify → cluster → consolidate → compress → archive → delete. Individual steps callable via classifyOnly(), archiveOnly(), consolidateOnly()
index.ts Re-exports

Updated files

File What changed
MemoryEngine.ts compaction getter (lazy CompactionPipeline), archiveBackend getter, compactionConfig getter, runCompaction() method, _buildCompactionMetadata(), _buildProtectedIds(). MemoryEngineOptions.compaction accepts Partial<CompactionConfig>.
index.ts Exports all Phase 4c modules and types

Compaction config defaults

{
  archiveAfterDays: 90,              // Memories older than this with no recall → cold
  warmImportanceThreshold: 0.4,      // Importance below this with few recalls → warm
  coldImportanceThreshold: 0.2,      // Importance below this + old → cold
  warmRecallThreshold: 3,            // Recalls below this → consolidation candidate
  clusterMinSize: 50,               // Minimum cluster size for LLM consolidation
  clusterMaxSize: 200,              // Maximum cluster size for LLM consolidation
  deleteAfterConsolidationDays: 30,  // Originals deleted 30 days after consolidation
  clusterEmbedDim: 64,               // Embedding dimension for clustering
  constitutionalGuardrails: true,    // Enable post-processing guardrails
}

3 Tiers

Tier What Storage Recall
Hot <90 days, high importance, frequent recall, pinned, in contradiction Full fidelity in Postgres + Qdrant Full vector search
Warm Medium age/importance, few recalls LLM-consolidated facts in Qdrant, originals archived Consolidated summary via vector search
Cold >90 days, never recalled, low importance Gzipped JSON archive files, not in Qdrant Not searchable via recall; restorable
Deleted Consolidated >30 days ago, originals purged Only the consolidated fact remains Consolidated summary only

Constitutional guardrails

The LLM summarization is protected by 4 post-processing checks:

  1. Fabrication check — verifies key terms in the summary appear in source episodes
  2. Instruction leakage detection — strips any text matching the system prompt structure
  3. Contradiction preservation — ensures source contradictions are noted in the summary
  4. Confidence scoring — assigns high/medium/low based on source count, pattern density, summary length

If guardrails detect issues, the LLM is retried once with an explicit warning.

Gotchas

  • LLM is BYOK — the user's API key is used; no TIMPS-hosted LLM. Falls back to rule-based concatenation when no LLM configured.
  • Archive is cold storage — archived entries are NOT in Qdrant and not vector-searchable. They can be restored via ArchiveBackend.restoreAll().
  • Protected IDs — memories involved in contradictions are protected from archival/deletion. The _buildProtectedIds() method checks all entries via ContradictionDetector.
  • CompactionPipeline can run individual stepsclassifyOnly(), archiveOnly(), consolidateOnly() for targeted operations.
  • MemoryEngine.runCompaction() applies changes (delete, compress) after the pipeline report is generated, then enqueues materialized view refresh.
  • Cluster count formula: max(1, round(sqrt(N/2))) — for 100 warm memories → 7 clusters, for 1000 → 22 clusters.
  • k-means++ initialization uses weighted random selection for the first centroid, then distance-squared weighting for remaining centroids.
  • ArchiveBackend stores gzipped JSON — each batch is one archive_{timestamp}.json.gz file. Index is a stripped manifest list.
  • Compression ratio for verbose content: typically 2-7x. Embedding is always preserved.
  • Pre-existing test failure: ContextVector — L19 > match returns empty when no similar contexts fails (unrelated to Phase 4c).

Phase 4d — Rust-Native Compute Engine (June 2026)

New files in packages/memory-core-rs/:

File Purpose
src/compute.rs 3 #[napi] functions: computeBatchSimilarity (cosine), kmeansClusterFlat (k-means++), eigenmodeWarmStart (power iteration)
src/lsh.rs RustLsh NAPI-RS class — murmurhash-based LSH (4 tables × 8 bits, embed dim 64), methods: insert/query/delete/size/clear/getAll
index.d.ts Manually-maintained TypeScript declarations for all napi exports

Updated files:

File What changed
src/lib.rs Fixed 17 pre-existing compile errors: added serde dep with derive feature, HashMap init fix, f32→f64 for napi params, partial move in load_model()
Cargo.toml Added which = "6" dep, serde = { version = "1", features = ["derive"] }
Cargo.lock Regenerated

New files in packages/memory-core/src/:

File Purpose
native.ts NativeCore + RustLSHNative interfaces, createRustLSH(), nativeBatchSimilarity(), nativeKMeans(), nativeEigenmodeWarmStart() wrappers. All return null when native addon unavailable.

Updated files in packages/memory-core/src/:

File What changed
native.ts Added Phase 4d wrappers (102 new lines). Fixed typo RustLSHRustLsh to match napi-rs naming.
compaction/ClusterEngine.ts _kMeansCluster() calls nativeKMeans() fast-path first, falls back to TS k-means++ with deterministic golden-ratio sin seeding
intelligence/contradiction.ts _rustLsh: RustLSHNative | null field. Constructor tries createRustLSH(). check() queries Rust LSH first (max 16 candidates), falls back to TS LSHIndex
storage.ts Removed getNative().jaccardSimilarity() call (function not in Rust addon)

Rust build

cd packages/memory-core-rs
npx napi build --platform --release
# Produces: memory-core-rs.{platform}.node (~800KB darwin-arm64)

Gotchas

  • napi-rs naming: Exported class name RustLsh (capital L, lowercase sh) — napi-rs capitalizes first letter of class names. The JS name is RustLsh, not RustLSH.
  • NAPI-RS Vec requires JS Array<number> — NOT Float64Array. The nativeKMeans() and nativeBatchSimilarity() wrappers convert Float64Array to Array<number> before calling into Rust.
  • Native addon is optional — all callers check getNative() for null and fall back to TypeScript. No crash if .node file is missing.
  • index.d.ts is manually maintainednapi build --no-dts was used. Type declarations must be kept in sync with src/compute.rs and src/lsh.rs.
  • streamInference streams via AsyncTask + ThreadsafeFunction<String> — signature is streamInference(modelPath, prompt, maxTokens, temperature, onToken) => Promise<string>. It shells out to llama-cli (if installed); without llama-cli it resolves finish_reason: "error" JSON. Do NOT change it back to a sync string return.
  • get_embedding is a 256-dim feature-hash embedding (unigrams + bigrams + char trigrams, FNV-1a hashing trick, L2-normalized, model name "local-feature-hash-256"). Semantic but not a real LLM — unrelated texts score ~0, shared tokens score >0.4.
  • LocalModel::from_path reads real GGUF headers via src/gguf.rs (architecture, name, vocab/embedding/layers/context, quantization). Filename heuristics are only a fallback when the GGUF header is missing.
  • k-means uses deterministic k-means++ — golden-ratio sin seeding replaces Math.random(). Same input always produces same clusters.
  • RustLsh is a stateful NAPI-RS class — maintains LSH tables on the JS heap. Each engine instance should create its own RustLsh instance.
  • jaccardSimilarity is NOT in the Rust addon — always uses the TypeScript implementation in storage.ts.
  • napi.targets must stay in sync with the index.js loaderpackages/memory-core-rs declares 8 triples (darwin x64/arm64, linux-gnu x64/arm64, linux-musl x64/arm64, win32-msvc x64/arm64). Do NOT use napi.triples.defaults (it expands to only 4 targets in @napi-rs/cli v3). Use napi.binaryName (not deprecated napi.name).
  • The loader is no-throwindex.js exports null (not a throw) when no .node binary exists for the current platform, so getNative() degrades to TypeScript. Set TIMPS_NATIVE_VERBOSE=1 to log why the addon didn't load.

Phase 4e — Caching Strategy (June 2026)

New files in packages/memory-core/src/cache/:

File Purpose
L1Cache.ts In-process LRU cache with TTL, stale-while-revalidate (15s grace), LRU eviction at maxSize (default 1000), pattern invalidation, scoped key generation via makeKey()
CascadeCache.ts Three-tier cascade: L1 (in-process LRU, <1ms) → L2 (Redis CacheManager, <5ms) → L3 (compute, <50ms). getOrCompute<T>(), invalidateProject(), warmup(), getStats()
ForgeCache.ts Specialized forge state cache with per-forge TTL overrides (echo:reservoir 60s, harmonic:eigenmodes 300s, aether:eigenmodes 300s, contradiction:pairs 60s). Keyed as forge:{org}:{proj}:{forgeName}:{stateType}
cache.test.ts 29 tests covering all three cache classes

Updated files:

File What changed
MemoryEngine.ts _cascadeCache field initialized in constructor. recall() uses getOrCompute() through cascade (cache key: recall:{query}:{type}:{limit}:...). store()/consolidate()/runCompaction() call invalidateProject(). warmupCache() method for startup pre-population. EngramLog-based EventBus subscriber invalidates cache when other servers store memories.
types.ts SearchOptions extended with useCache?: boolean and cacheTTL?: number
index.ts Exports CascadeCache, ForgeCache, L1Cache + option types

Cache architecture

recall() request
  │
  ├─ L1 (in-process LRU, <1ms) ──→ HIT → return
  │     TTL: 5s, stale grace: 15s
  │     Eviction: LRU at 1000 entries
  │
  ├─ L2 (Redis CacheManager, <5ms) ──→ HIT → populate L1 → return
  │     TTL: recall=60s, forge=300s
  │     Backend: CacheManager (Redis SCAN+DEL)
  │
  └─ L3 (compute, <50ms) → populate L2 + L1 → return
        Called only on complete miss

Invalidation

Trigger Action
store() invalidateProject() — clears all L1+L2 keys with current scope prefix
consolidate() Same, when entries are actually removed
runCompaction() Same
Remote memory:stored event (EventBus) Same — cross-server cache consistency

Cache key format

global:{normalizedQuery}                              (no org scope)
{orgId}:{projectId}:{normalizedQuery}                  (with org scope)
{orgId}:{projectId}:{normalizedQuery}:{suffix}         (with suffix)
forge:{orgId}:{projectId}:{forgeName}:{stateType}      (forge state)

Gotchas

  • CacheManager.invalidatePattern() prepends its own keyPrefixCascadeCache.invalidateProject() passes the raw scope prefix (org:proj:) without a cache:* prefix. The CacheManager adds the prefix internally.
  • L1 stale-while-revalidate — stale entries within the grace period (default 15s) are returned immediately. The next request triggers a fresh compute. No background refresh is scheduled.
  • ForgeCache TTLs are separate from recall cache TTLs — forge state (eigenmodes, reservoir) has longer TTLs (up to 300s) since it changes less frequently.
  • EventBus.subscribe() returns Promise<void> — not an unsubscribe function. Store the handler reference for later cleanup via unsubscribe().
  • Cache warmupMemoryEngine.warmupCache() calls CascadeCache.warmup() which pre-populates both L1 and L2. Useful on MemoryServer startup to avoid cold-start latency.
  • EngramLog-based invalidation — the EventBus subscriber is set up in the MemoryEngine constructor. It filters by projectId to avoid invalidating irrelevant projects. Cleaned up in dispose().

Phase 5c — LSP Integration (June 2026)

New files in timps-code/src/services/lsp/:

File Purpose
protocol.ts LSP JSON-RPC 2.0 types, encodeLspMessage(), decodeLspMessages()
proxy.ts LspProxyServer — wraps real language servers, intercepts definition/hover, publishes contradiction & bug-pattern diagnostics
proxy-entry.ts Standalone CLI entry point (node proxy-entry.js --language=typescript --stdio)
lsp.test.ts 16 tests covering protocol, proxy handlers, graceful degradation, document state management

New file in timps-vscode/src/:

File Purpose
lsp-client.ts TimpsLspClient — spawns proxy as child process, bridges diagnostics to VS Code, registers definition+hover providers

Updated files:

File What changed
timps-code/src/services/lsp/manager.ts forwardToRealServerWait() — fast-fail when no server, reduced timeout from 10s to 5s
timps-vscode/src/extension.ts LSP client init on activate, document sync events, provider registration, toggle command
timps-vscode/package.json LSP settings (enabled, debounceMs, contradictionSeverity), timps.toggleLsp command

LSP proxy architecture

VS Code                              TIMPS LSP Proxy                      Real Language Server
  │                                       │                                       │
  │── textDocument/definition ──────────→ │── textDocument/definition ──────────→ │
  │                                       │←── Location[] ────────────────────────│
  │                                       │── MemoryClient.recall(filename) ──→  │
  │←── Location[] + relatedFiles ──────── │                                       │
  │                                       │                                       │
  │── textDocument/didChange ──────────→ │── (debounce 2s) ──→                    │
  │                                       │── MemoryClient.checkContradiction ──→ │
  │←── publishDiagnostics (contradictions)│                                       │
  │                                       │                                       │
  │── textDocument/didSave ────────────→ │── MemoryClient.checkBugPattern ──────→ │
  │←── publishDiagnostics (bug patterns) │                                       │

Key design decisions

  • Proxy pattern — TIMPS adds memory data on top of real language server capabilities; not a replacement LSP server
  • VS Code registers TIMPS as additional provider — not replacing the built-in language client; both sources merge
  • Proxy runs as child process (spawn with node proxy-entry.js --stdio) — isolation, independent restart
  • Contradiction on didChange (debounced 2s), bug pattern on didSave — saves CPU
  • forwardToRealServerWait rejects immediately when no server — avoids 10s timeout
  • MemoryClient is an interface — enables testing without running MemoryServer

Gotchas

  • 15/16 LSP tests pass — 1 graceful-degradation test accepts timeout via .catch() (the server doesn't crash, which is the point of the test)
  • Pre-existing executor.ts errors remain (3x seedEngineWithDataset property missing) — unrelated to LSP work
  • LSP proxy uses fetch for MemoryClient HTTP calls — requires Node.js 18+ (built-in fetch available)
  • LspLocationLink type — Added to protocol.ts exports. Definition handler casts results with 'uri' in loc type-narrowing check.
  • Test mock LSP server — inline Node.js script (lspMockScript) provides minimal LSP responses, avoids real language server binaries
  • process.execPath used for spawn in tests — avoids ENOENT errors from which node
  • Debounce timers are cleared on didClose — prevents phantom contradiction checks after document is closed
  • JetBrains stub investigated — none found — no JetBrains plugin files exist in the codebase

Phase 5d — SDK & Embedding (@timps-ai/timps-sdk)

New package at packages/sdk/:

File Purpose
package.json Dual CJS/ESM exports, sideEffects: false, optional peer dep on @timps/memory-core
tsconfig.json ESM module resolution for tree-shakable builds
src/index.ts Exports createMemory(), Memory interface
src/MemoryClient.ts Memory wrapper over MemoryEnginestore, recall, delete, storeBatch, on, getStats, dispose
src/defaults.ts Runtime detection (Node/Bun/Deno), provider configuration (Ollama/OpenAI/Anthropic/none)
src/types.ts Public types: MemoryOptions, ProviderConfig, Provider, RecallOptions, MemoryEntry
sdk.test.ts Tests — createMemory, store/recall, provider config, runtime detection
README.md 3-line quickstart that works immediately

SDK architecture

@timps-ai/timps-sdk — lightweight user-facing package (~50KB)

  createMemory({ projectPath, provider? })
       │
       ├─ MemoryEngine({ backend: FileBackend(memoryDir(projectPath)) })
       │     ├─ store() → EchoForge → FileBackend.write()
       │     ├─ recall() → MiniSearch BM25 search → return results
       │     │     └─ (if provider set) EmbeddingService.embed()
       │     ├─ delete() → remove from semantic store
       │     ├─ storeBatch() → bulk store
       │     ├─ on('stored' | 'error') → event emitter
       │     ├─ getStats() → { totalMemories, storageSize, lastUpdated }
       │     └─ dispose() → cleanup
       │
       └─ Provider detection:
             'ollama' → localhost:11434, nomic-embed-text
             'openai' → api.openai.com, text-embedding-3-small
             'anthropic' → api.anthropic.com, voyage-2
             null → keyword search only (no vector deps)

What the SDK does NOT import from memory-core

  • gRPC server/client, WebSocket server, REST server/routes
  • PostgresBackend, RedisBackend, QdrantBackend
  • CRDT engine, Compaction pipeline, Migration engine
  • Eval framework, OpenTelemetry, ConstitutionalSandbox
  • ProjectRoom, 14 Rust crates
  • Express, cors, ws, @grpc/grpc-js, ioredis, @qdrant/js-client-rest

Package configuration

{
  "name": "@timps-ai/timps-sdk",
  "sideEffects": false,
  "exports": {
    ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" }
  },
  "peerDependencies": { "@timps/memory-core": ">=1.0.0" },
  "files": ["dist"]
}

Quickstart

npm install @timps-ai/timps-sdk
import { createMemory } from '@timps-ai/timps-sdk'

const memory = createMemory({ projectPath: '.' })
await memory.store('This project uses tRPC for type-safe APIs')
const results = await memory.recall('API patterns')
console.log(results)
// [{ content: 'This project uses tRPC...', score: 0.92 }]

Gotchas

  • Provider detection is optional — no provider = keyword search only (BM25 via MiniSearch), zero network deps
  • createMemory() does NOT start a server — it's a local-only client. No gRPC, no REST, no WebSocket
  • Runtime detection is automatic — Node.js fs, Bun Bun.file, Deno Deno.readTextFile — all handled transparently
  • @timps/memory-core is an optional peer dependency — the SDK can work without it when bundled (imports are type-level where possible)
  • Tree-shaking — with sideEffects: false and ESM exports, bundlers eliminate unused forge layers and backends
  • Published size <50KB, tree-shaken to <30KB for basic usage
  • Memory.dispose() flushes the embedding queue and stops background computation — call on shutdown
  • store() is synchronous for local storage — embedding computation is fire-and-forget, BM25 handles immediate recall
  • SDK shares the canonical memory storecreateMemory() writes to ~/.timps/memory/<projectHash> via memoryDir(projectPath), the same location the CLI and memory dashboard read from. Memories written by the SDK are visible to timps and the dashboard, and vice versa. An explicit MemoryOptions.dir opts into a custom store instead.
  • One-time legacy migration — on first use, MemoryClient.initialize() copies data from the legacy project-local store (<project>/.timps/memory) into the canonical store when the canonical store is empty. It skips .wal files and never overwrites existing canonical data; it is skipped entirely when an explicit dir is provided. (M70)

Phase 5e — International Team Features (June 2026)

New files in packages/memory-core/src/:

File Purpose
MemoryBranch.ts Git-style decision branches: MemoryBranchStore with createBranch, commit, getHistory, merge, listBranches, deleteBranch

Updated files in packages/memory-core/:

File What changed
types.ts Added BranchCommit, BranchMetadata, AuditQuery, AuditResult, TeamDigest, TeamDigestEntry, PlatformMetadata types. Extended MemoryEntryType with 'decision'. Extended SearchOptions with actorIds?, platform?, importance?.
MemoryEngine.ts Added audit(), getTeamDigest(), createBranch(), branchCommit(), getBranchHistory(), listBranches(), mergeBranches(), deleteBranch() methods. store() now accepts platform and channel in extended entry.
index.ts Exports MemoryBranchStore, BranchCommit, BranchMetadata, AuditQuery, AuditResult, TeamDigest, TeamDigestEntry, PlatformMetadata

New file in timps-code/src/commands/:

File Purpose
audit.ts runAuditCommand() — CLI handler for timps audit --member <name> --since <date> with table-formatted output

Updated files in timps-code/:

File What changed
src/commands/executor.ts Added /audit command route to runAuditCommand. Added /team:digest command route.
src/memory/memory.ts Added audit(), getTeamDigest() methods bridging to MemoryEngine

Phase 5e architecture

store(content, { platform: 'wechat', channel: 'team-backend' })
  │
  ├─ Platform stored as metadata on MemoryEntry (not a scope boundary)
  ├─ EngramLog records actorId + platform + channel
  ├─ Recall() ignores platform — cross-platform results
  └─ Timeline: entry stored with timestamp for digest purposes

audit({ actorId: 'alice', since: Date })
  │
  ├─ Queries EngramLog JSONL for matching actorId
  ├─ Groups by type (decision, bug_fix, pattern, code_change)
  ├─ Returns sorted by timestamp descending
  └─ CLI format: table with timestamp, type, content, project columns

getTeamDigest({ since: lastSession, types: ['decision','bug_fix','pattern'] })
  │
  ├─ Queries semantic entries matching type filter since timestamp
  ├─ Filters by importance (high-confidence decisions, recent bug fixes)
  ├─ Formats as human-readable markdown digest
  └─ Returns: { entries, summary, generatedAt }

MemoryBranchStore
  │
  ├─ createBranch(name, description, creator) → branch metadata
  ├─ commit(branch, content, reason, author) → BranchCommit with parent ref
  ├─ getHistory(branch) → sorted commits, oldest first
  ├─ merge(source, target, strategy) → merge commit or conflict
  ├─ listBranches() → all branches with head info
  └─ Stored under `branches:{name}:meta`, `branches:{name}:commits` keys

Branch-aware recall

When recall() finds a decision-type entry that has a matching branch:

recall("database choice") → [{
  branch: "database-choice",
  history: [
    { content: "Use MongoDB", author: "alice", date: "Jan 15" },
    { content: "Switch to PostgreSQL", author: "bob", date: "Mar 3" },
    { content: "Add Redis for sessions", author: "alice", date: "Jun 20" },
  ],
  currentHead: "Add Redis for sessions",
  mergedFrom: ["bob"]  // if merge was involved
}]

Branch conflict detection

When two commits are made to the same branch with conflicting directions:

Alice commits: "Migrate Redis to KeyDB for multi-threading"
Bob commits:   "Migrate Redis to Dragonfly for memory efficiency"

→ Branch conflict (two concurrent heads on same branch)
→ TIMPS surfaces at next recall: "This branch has a conflict"
→ Team resolves: choose one, or create merge commit

Cross-timezone timestamps

  • All timestamps stored as UTC ms since epoch (Date.now())
  • Display formatting: Intl.DateTimeFormat with detected or configured timezone
  • CLI format: Jun 25 7:15pm IST (timezone abbreviation from Intl API)
  • Digest header: shows user's timezone vs stored UTC offsets

Gotchas

  • Platform is metadata, not scopeOrgScope defines the isolation boundary (org + team + project). Platform (wechat, slack, discord) is stored on MemoryEntry.tags and the EngramLog payload, but does NOT affect isolation. A user on Slack can see WeChat memories.
  • Branch commits are stored as JSON array under branches:{name}:commits — not in the semantic store. This keeps branches separate from the main recall pipeline. Branch-aware recall enriches results by cross-referencing decision entries.
  • audit() reads from EngramLog, not from the semantic store. The EngramLog already records actorId, op, entryId, and payload for every store operation. Audit just queries this log.
  • Team digest filters by type and actorId — not by full-text search. A digest is a summary of recent team activity, not a recall query. Use recall() for full-text search and getTeamDigest() for daily summaries.
  • Branch merge uses last-writer-wins by default — the most recent commit's content becomes the new head, with the older commit recorded as mergedFrom. This matches git's merge commit pattern.
  • Branch conflict detection is synchronouscommit() checks the current head against the new commit's sentiment direction. If they appear conflicting, the commit is still stored but crdtStatus: 'conflict_pending' is set on the branch metadata. Call listBranches() with showConflicts: true to surface pending conflicts.

Phase 6c — Self-Improving Agent Loop

The SelfImprovingAgent (timps-code/src/agent/selfImprovingAgent.ts) is an autonomous feedback loop that tracks mistakes, learns from them, and injects prevention instructions into the agent's system prompt to avoid repeating errors.

Classes and types

Symbol File Purpose
MistakeCategory selfImprovingAgent.ts Union: wrong-file-assumption, insufficient-context, incorrect-tool-sequence, missed-dependency, test-regression, type-error, logic-error, permission-violation, infinite-loop, over-engineering, under-specification, tool-misuse, other
MistakeRecord selfImprovingAgent.ts Full mistake record: id, timestamp, category, taskType, description, errorMessage, correction, preventionHint, occurrences, filePatterns, toolSequence
LearningReport selfImprovingAgent.ts totalMistakes, topCategories, recentMistakes, preventionInstructions, improvementScore
PreflightCheck selfImprovingAgent.ts passed, warnings, blockers, suggestedActions
SelfImprovingAgent selfImprovingAgent.ts Main class: recordMistake(), recordCorrection(), preflightCheck(), buildLearningReport(), buildPreventionInstructions(), formatForSystemPrompt(), generateTrainingData(), buildPromptImprovements(), getSessionSummary()

Integration points

In timps-code/src/core/agent.ts:

  • formatForSystemPrompt() called during system prompt construction
  • preflightCheck() called before each user message execution
  • recordMistake() called on tool execution failure (isError=true)

Persistence

Mistakes stored as individual JSON files (m_<timestamp>_<random>.json) in ~/.timps/learning/<projectHash>/.

CLI commands (timps-code)

Command Handler Description
improve runImproveCommand() Full learning report + training data
improve:report runImproveCommand() Report only
improve:train runImproveTrainCommand() GRPO training data generation
improve:prompt runImprovePromptCommand() System prompt additions

Gotchas

  • File-based persistence — mistakes are individual JSON files, not stored via MemoryEngine. Each file is atomic (write + rename).
  • Similar mistake deduprecordMistake() increments occurrences when jaccardSimilarity(desc, existing) > 0.8 instead of creating a new record.
  • Preflight warnings have a limitpreflightCheck() caps warnings at 5 and blockers at 3 to avoid overwhelming the agent.
  • formatForSystemPrompt() returns empty string when no patterns — the agent prompt is not bloated until sufficient learning data exists.
  • Training data is GRPO-compatiblegenerateTrainingData() produces OpenAI-style messages format for fine-tuning pipelines.

Phase 6d — Multimodal Memory

Sovereign multimodal memory layer (timps-code/src/memory/multimodalMemory.ts) supporting image, audio, and text embeddings via Ollama (Gemma 3 + Nomic-embed-text), with cross-modal recall, diagram storage, terminal capture, and storage budget enforcement.

Architecture

storeImage(path) ──→ GemmaEmbedder.encodeImage() ──→ LocalVectorStore.insert()
storeText(text)  ──→ GemmaEmbedder.encodeText()  ──→ LocalVectorStore.insert()
search(query)    ──→ GemmaEmbedder.encodeText()  ──→ LocalVectorStore.search() ──→ cosine similarity
crossModalRecall ──→ search each modality ──→ expand linked entries ──→ rank & return

Key files

File Purpose
src/memory/multimodalMemory.ts MultimodalMemory, GemmaEmbedder, LocalVectorStore
src/commands/multimodalCommands.ts Slash command handlers for /vision, /audio, /recall, /screenshot, /diagram, /terminal, /crossmodal, /mmbudget, /visionstats
src/services/objectStorage.ts ObjectStorage — local-first object storage with optional S3 fallback
src/core/agent.ts Integration point for multimodal commands at app.ts ~line 1391

MultimodalMemory methods

Method Description
storeImage(path, tags?) Store image with Gemma vision embedding
storeImageBase64(base64, mimeType, tags?) Store base64-encoded image
storeAudio(path, duration, tags?) Store audio file reference
storeText(text, tags?) Store text entry
search(query) Multi-modal search (text/image/audio query + tag filter)
findSimilarImages(path, limit?) Image-to-image similarity
findRelatedText(text, limit?) Text-to-all search
storeDiagram(path, diagramType, tags?) Store diagram with type classification
captureTerminal(command, output, exitCode, tags?) Store terminal output as text memory
crossModalRecall(query, opts?) Search all modalities + linked entry expansion
linkEntries(idA, idB) Create cross-modal links
setStorageBudget(maxBytes) Set storage budget (default 100MB)
enforceStorageBudget() Prune least-accessed entries when over budget

CLI commands

Command Aliases Description
/vision /vis Store or search images
/audio /sound, /voice Store or search audio
/recall /remember Recall multimodal memories by text query
/screenshot /ss, /screen Capture macOS/Linux screenshot
/diagram /chart, /plot Store diagram with type classification
/terminal /cmd, /command Capture terminal command output
/crossmodal /cm, /query-all Cross-modal recall
/mmbudget /storage, /budget Show storage budget usage
/visionstats /vstats Show multimodal memory statistics

Gotchas

  • Ollama is optionalGemmaEmbedder gracefully degrades to deterministic Math.sin/Math.cos fallback embeddings when Ollama is unavailable.
  • LocalVectorStore uses JSONL — stored at <memoryDir>/multimodal.jsonl, no external vector DB required.
  • Cross-modal searchcrossModalRecall() searches all modalities separately, then expands results via getLinkedEntries(). Linking is manual (linkEntries()).
  • Storage budget — default 100MB. enforceStorageBudget() prunes least-accessed entries when over budget.
  • S3 integrationObjectStorage supports opt-in S3 via @aws-sdk/client-s3 (lazy-loaded). Default is local ~/.timps/objects/ with index.json tracking.
  • Screenshot capture — uses screencapture (macOS) or import (Linux, ImageMagick). Requires ImageMagick on Linux.

Phase 6c+6d — Bug fixes included

Latest commit (acf3383) fixed these issues alongside the new features:

  1. timps-mcp/src/index.ts — Added await to 5+ async recall() calls
  2. timps-code/src/memory/memory.ts — Delegated branch ops to MemoryBranchStore instead of nonexistent MemoryEngine methods
  3. packages/memory-core/src/index.ts — Added FileBackend, InMemoryBackend, seedEngineWithDataset exports
  4. packages/sdk/src/MemoryClient.ts — Fixed FileBackend import, EmbeddingConfig fields, null-safety
  5. timps-code/src/plugins/pluginManager.ts — Fixed implicit any type error
  6. install.sh — Switched from cd + npm run build to npm run build --workspace=timps-code

Updated gotchas

  • Server deps already fixedpackages/server/package.json already has @timps-ai/memory-core in dependencies and typescript in devDependencies. The AGENTS.md notes about missing deps and eval import errors in executor.ts are stale — all exports resolve correctly.
  • supply-chain-audit.yml — already uses google/osv-scanner-action@v2.3.8 (upgraded from v1.0.2).