Canonical instruction doc for AI agents (OpenCode, Copilot, Claude, Cursor). If this conflicts with the README, trust this file.
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.
timps-code→ ESM (module: NodeNext),.jsextension in imports required.timps-mcp→ CJS,strict: false, no declarations.packages/server→ CJS, wide include.packages/memory-core→ CJS, vitest resolves.jsimports automatically.
- CLI:
src/bin/timps.ts(run vianpm run devortsx src/bin/timps.ts). - Loop:
src/core/app.ts→AgentLoop.run()→src/core/agent.ts. - Tool registry:
src/tools/tools.tsexportsALL_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 atsrc/tools/mcpDiscovery.ts.
timps setupinstalls 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: truefrontmatter). Windsurf has no instruction file (MCP only). Seetimps-code/src/commands/setup.ts(installInstructions/uninstallInstructions).- Idempotent + reversible: re-running
timps setupis a no-op;--uninstallremoves the block;--no-instructionsskips it;--dry-runpreviews.--binary <path>points at a specific MCP binary (default isnpx -y @timps-ai/timps-mcp, which only works once published). - CLI commands in
timps-code/src/commands/recall.ts(wired insrc/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 bothTIMPS_URLandTIMPS_MEMORY_URL(inbuildRegistration()), because timps-mcp's memory tools (timps_store_memory,timps_get_memories,timps_check_contradiction) route throughMemoryClientonly whenTIMPS_MEMORY_URLis set AND server mode is on (TIMPS_URL). Override either viaTIMPS_SETUP_ENV=TIMPS_MEMORY_URL=… timps setup …. Legacypackages/server(port 3000) is the only thing that serves/api/*(timpsAPIchatproxy); 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). Entrydist/server/start.jsmounts eval routes → needs writable project path at startup. projectHash()canonicalizes withfs.realpathSyncso 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 touchpackages/memory-core/src/storage.ts(that package is git-ignored/hidden here; rebuild its dist sotimps-codepicks up the change).- End-to-end contract: agents store via
timps_store_memoryMCP (project dir → canonical hash), terminal reads viatimps recall/timps contexton the same canonical hash — they always converge on one store.
packages/memory-core/— canonical, source of truth for 25 intelligence tools.timps-code/src/memory/— thin adapter (337 lines), delegates toMemoryEngine.packages/server/memory/— thin adapters overMemoryEngine, 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().
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'].
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._backendWhen 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.
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.jsonl → episodes.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:
- Create
vN_to_v{N+1}.tsexporting aMigrationobject - Add it to
ALL_MIGRATIONSinmigrations/index.ts - Bump
CURRENT_SCHEMA_VERSIONinmigrations/types.ts - 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.
- ESM in
timps-code; CJS elsewhere. - No pre-commit hooks, no
.cursor/rules/, noCLAUDE.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')(notMath.random()).
timps-mcptool 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
MemoryEnginelazy getter names. StorageBackendinterface methods (backends must stay compatible).- Episodic storage format (
episodes.jsonJSON array — not JSONL).
-
npx tsc --noEmitpasses in affected package (--max-old-space-size=4096for timps-mcp). - Tests pass:
npm test(root vitest run). - Coverage passes:
npm run test:coveragemeets 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_TOOLSand added smoke test tobenchmark/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.mdif public API changed. - If you added a new StorageBackend, implement all interface methods including
beginTxn(),exists(),append(), andquery(). - If you added a forge layer, ensure it accepts
backend?: StorageBackendas a constructor param afterdirand usesthis._backend.read/writewhen available.
npm run buildbuilds all packages (mobile/plugins/docs may need extra tooling). Usebuild:ciin CI.packages/serverpackage.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?)defaultsautoStore=true— passfalsein 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) orvitest runfor all tests. Coverage:npm run test:coverage(80% threshold enforced). packages/serveris missing@timps/memory-corein its package.jsondependencies— it's installed by hoisting but won't resolve innpm ci --production.timps-desktophas a JSX parsing issue inglobal.d.ts(missing space beforedeclarekeyword after a JSX block). Ifnpx tsc --noEmitfails for timps-desktop, check.d.tsfiles for similar syntax issues.test-coverage.ymlmatrix includesconfig,integration-basepackages that no longer exist. Drop them from the matrix if they're removed from the workspace.supply-chain-audit.ymlusesgoogle/osv-scanner-action@v1.0.2(old). Update tov2.3.8if the action fails.packages/servercheckscript istscbut TypeScript isn't installed as a devDependency — it's hoisted from the root. CI may fail on strict isolated environments unlesstypescriptis added to its devDependencies.
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 |
MemoryEngineOptions.cacheManager?: CacheManager— Redis cache for forge state (get-or-compute with TTL)MemoryEngineOptions.eventBus?: EventBus— Redis Pub/Sub for cross-server eventsMemoryEngine.cacheManager/MemoryEngine.eventBus— gettersstore()publishesmemory:storedevent,recall()publishesmemory:recalled(for queries >10 chars),consolidate()publishesmemory:consolidatedMemoryServerOptions.eventBus?: { url? } | false— enables EventBus in server, auto-injects into engineMemoryServerOptions.serverId?: string— server identity for event bus message dedupGET /health/readiness— probes Postgres, Redis, EventBus, Cache, returns 503 if any failPostgresBackendnow takesprimary(writes) + optionalreplicas[](reads, round-robin)PostgresBackend.health()— ping primary + first replica- Exports:
EventBus,CacheManager,QdrantBackend(via backends index)
PostgresBackendconstructor changed: wasconnectionString, nowprimary+ optionalreplicas[].- EventBus requires
ioredisat runtime (lazyrequire). CacheManager also requiresioredis. - QdrantBackend requires
@qdrant/js-client-restat runtime (lazyrequire). - MemoryServer forwards event bus messages to WebSocket clients via
wsServer.broadcast(). - The
memory:storedevent payload includesid,content(truncated 200),type,tags,confidence,actorId. - The
memory:recalledevent only fires for queries >10 chars to reduce noise. - Scale out:
docker compose up -d --scale memory=3spins 3 server instances. - Secrets: compose uses
${POSTGRES_PASSWORD:-...}/${REDIS_PASSWORD:-...}/${GRAFANA_ADMIN_*:-...}env substitution — override with a.envfile (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 tofalse. - Streaming replicas (M42):
postgres-replica-1/2are TRUE streaming replicas, not empty standalones. Primary boot runsdeploy/postgres/init-replication.sh(createsreplicatorREPLICATION LOGIN role + appendshost replication all 0.0.0.0/0 scram-sha-256topg_hba.conf+ reload — the stockpostgres:16-alpineimage ships NO TCP replication rule, so a hand-writtenprimary_conninfocommand would fail withno pg_hba.conf entry for replication). Replicas override the entrypoint withdeploy/postgres/replica-entrypoint.sh: on empty data dir they ensure a physical slot on the primary andpg_basebackup -X stream -R, then appendprimary_conninfo(with password +application_name) and a standaloneprimary_slot_nameGUC topostgresql.auto.conf. Gotchas: (1)primary_slot_nameis a server parameter, NOT a libpq conninfo option — putting it insideprimary_conninfoerrors withinvalid connection string syntax; (2) replica healthcheck ispg_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, andpsqlneeds-d timps_memory(defaulting to the username givesdatabase "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 skipspg_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.
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. |
MemoryEngine.getSemanticEntries()returns the raw semantic entries array (not a copy). Mutation is safe sinceloadSemanticre-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 Conflictresponse is returned with theConflictEventpayload. - 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.subscribeRawreturns an unsubscribe function. ProjectRoom stores this for cleanup ondestroy().- gRPC
AgentStreamnow pushesConflictEventmemory insights to agents when theycheck_conflictsor emitstored_memoryevents. ProjectRoomauto-subscribes toroom:{projectId}:eventson Redis. WhenagentCountdrops to 0,destroy()is called and the subscription is released.- The
memoryCoordinator.tsSSE stub intimps-codeis now a shell — all real-time coordination goes through the MemoryServer gRPC/WebSocket endpoints.
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):
StorageBackendinterface — new methodssetScope(scope),getScope(). Allread/write/delete/list/exists/appendaccept optionalscopeparam that overrides active scope.InMemoryBackend— scope-aware Map storage,_activeScopestate,_resolveScope()picks active scope or explicit param, keys prefixed withmemory:{org}:{team}:{project}:{key}.PostgresBackend—setScopemanages session-levelorg_id/team_id/project_idviaSET SESSIONvariables; RLS policies onmem_storetable;buildKeygenerates scoped keys for non-RLS tables.RedisBackend—setScopesets_activeScope, all keys prefixed with scope prefix for logical database partitioning.QdrantBackend—setScopesets_activeScope, upsert/search injectorg_idpayload filter to enforce tenant isolation. Renamed internal_generateId→_generateUuidfor clarity.FileBackend— unchanged (no scope support; scope-aware backends are the future).
MemoryEngine changes:
- Accepts
orgScopeinMemoryEngineOptions. - Constructor calls
backend.setScope(orgScope)and passesthis._backendto all storage functions (loadSemantic(dir, backend),saveSemantic(dir, data, backend), etc.) — fixing the previous gap wherestorage.tsfunctions bypassed the engine's backend. - New method
multiProjectRecall(query, projectIds, options?)— iterates project scopes, temporarily switches backend scope, callsrecall(), 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)— readsx-org-id,x-team-id,x-project-idheaders from request-like objects. - New getter
backend— exposes the underlyingStorageBackend. store()enriches stored entries withorg:,team:,project:tags fromorgScope.
Auth middleware (src/server/auth.ts):
requireOrgScopemiddleware readsx-org-id/x-team-id/x-project-idheaders and attaches them to the request.- Token/API key auth can now include
orgClaim,teamClaim,projectClaim. requireAuthexported alongsideauthenticateRequest.
MemoryServer changes (src/server/MemoryServer.ts):
- Rate limiter middleware (
rateLimiter.check(orgId, endpoint)) on all write endpoints. GET /health/readinessprobes Postgres, Redis, EventBus, Cache, RateLimiter.- Server creates
RateLimiterinstance, 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.jsonsidecar withdefaultScope: { orgId: "default", projectId: dirNameHash }. - Adds
orgScopeto_metablocks of layer state files. - Added to
ALL_MIGRATIONS,CURRENT_SCHEMA_VERSIONbumped to 4.
New exports (src/index.ts):
RateLimiter,OrgScopetype,buildKey,scopeListPrefix,deriveProjectId.
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.
- 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:
multiProjectRecallworks by temporarily switching scope on the engine's backend. Engines with separate backends cannot cross-project recall. Use a singleInMemoryBackendshared across scope-managed engines in tests. - Pre-existing test failure:
ContextVector — L19 > match returns empty when no similar contextsfails 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.multiProjectRecallrestores the original scope after each project iteration.- Migration v3→v4 does NOT wrap data files:
episodes.json,semantic.json,working.jsonare raw arrays. They get a.org-scope.jsonsidecar instead of being wrapped with_meta. Layer state files (with_meta) get inlineorgScopemetadata. CURRENT_SCHEMA_VERSIONis now 4. Bump for any on-disk format change.
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 |
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 │
- 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 execution —
wasmtimeCLI 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
- Scanner requires a base64-decoded payload —
runStaticAnalysis(payload, manifest)takes base64-encoded plugin content PluginRegistry.submit()rejects on checksum mismatch before any analysis — saves CPU on invalid submissionsWasmSandbox.executeJS()creates a temporary script per execution — permissions are baked into the script via__permissionsconst- Marketplace frontend uses
NEXT_PUBLIC_MARKETPLACE_APIenv var — defaults tolocalhost:4100, configure in production - CLI's
pluginInstallauto-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
availablemap 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 — L19still fails (unrelated to Phase 3a)
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 |
MemoryServer (instrumented)
│
├─ /metrics (Prometheus text) ←─ Prometheus (scrape)
├─ POST /metrics/reset
└─ OTLP exporter (optional) → OTel Collector → Jaeger/Grafana Tempo
| 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 |
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.
4 pre-built dashboards at deploy/grafana/dashboard-*.json (all queries reference only metrics the server actually emits — verified, no dashboard-eval.json):
- Memory Health — stores by layer (stacked), contradiction rate, semantic-entries growth
- Performance — recall/store latency p50/p95/p99, backend breakdown, ops/sec
- Multi-Agent Activity — conflict detections + conflict checks, consolidation merge latency
- System — backend ops/latency, error rate, guard rejections, store/recall/consolidate throughput, semantic entries (no external exporters required)
- 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-nodeas an optional peer dependency. - Proxy-based instrumentation.
instrumentLayer()usesProxyto intercept IMemoryLayer methods without modifying forge classes. Non-IMemoryLayer methods pass through transparently. - Anonymous export runs hourly. The
TelemetryManagersets asetIntervalthat callsonAnonymousExportwith the redacted payload. Wire this to an HTTP endpoint or file sink in production. - Prometheus
/metricsendpoint is mounted at the MemoryServer when telemetry level islocaloranonymous. Prometheus discovers everymemoryreplica individually viadns_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 onotel-collector:8888, which re-exports anything pushed via OTLP (docker-compose exposes8888:8888).
- 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
telemetryfield inMemoryEngineOptionsis 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.wrapAsyncdetects 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 perstore()writecrdt.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 perconsolidate()run- Accessible via
engine.crdtMetrics(no-ops when telemetry off).
- Storage gauge:
memory.semantic_entriesis emitted on everystore()(nomemory.storage_size_bytesmetric exists — the dashboards usetimps_memory_semantic_entries). - No eval dashboard. Eval runs (
timps eval:run) are batch CLI processes that Prometheus never scrapes; there is deliberately nodashboard-eval.jsonindeploy/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 — L19still fails (unrelated to Phase 3b).chaos.test.tsalso has 2 pre-existing failures (cross-instance test,concurrent-write-*recall counts) from the documented short-string Jaccard dedup gotcha.
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 |
Bug fixes:
packages/memory-core/src/eval/storage.ts—InMemoryBackend.read()returns already-parsed object; removed extraneousJSON.parse()on the return value that caused double-parse errors.packages/memory-core/src/eval/regression.ts—RegressionDetector.check()whenbaselineValue === undefinedmust still comparemetric.valueagainstthreshold. Previously a missing baseline file made the gate pass everything.timps-code/src/commands/executor.ts—eval:runhandler: await asyncevaluateDataset()call (was returning Promise to CLI instead of EvalResult). Regression summary formatting:seenBaselinesMap type narrowed fromMap<string, any>toMap<string, MetricInfo>.
Bridge stub deletion:
- Deleted
timps-code/src/services/bridge.ts(85 lines) — empty stubs forbridge.cloud.sync(),bridge.monitor.getStatus(),bridge.plugins.getAnalytics(),bridge.billing.getSavedAmount(). - Deleted
timps-code/src/services/__tests__/bridge.test.ts— tests for deleted stubs.
- 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
providerMeshhandles 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).
keyVault.encrypt()output format ishexIV:hexTag:hexCiphertext.isEncrypted()tests with regex/^[0-9a-f]{32}:[0-9a-f]{32}:[0-9a-f]+$/.providerRateLimiterstores 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-keyhandler changes the old plaintext key to the encrypted version in-memory, then saves; it does NOT add a separate encryption step on load sinceconfig.tsauto-decrypts.
recall() is now async — all callers must await it.
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 |
- 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.
recall()is now async. All callers must useawait. This includesMemoryEngine.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 vectorbm25for native BM25-vector search. - Sparse vector format:
{ indices: number[], values: number[] }where indices are hash values (0-65535) and values are1 + 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.tsandmemory-unified.test.tsboth testrecall()synchronously withawait. If a new test file callsrecall()withoutawait, the result will be a Promise, not an array — the test will silently pass with wrong assertions.
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[]. |
- ComputationQueue follows EmbeddingQueue pattern — in-memory array + StorageBackend crash recovery, not Redis-dependent.
- All forge incremental updates are fire-and-forget —
store()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.
- 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?). UsescachedVectors[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 — notundefined. Consumers get a valid number. - LSH query candidates are limited to 16 per claim — tunable by
maxResultsparameter onquery(). Falls back to full O(N) scan when LSH returns 0 candidates. - ContradictionDetector
check()callsthis._lsh.query(claim, 16)— returnsstring[]IDs. TheautoStore=truedefault still stores each claim after checking. - MemoryEngine
_computationHandlers()register per-task-type handlers —eigenmode,contradiction,decay_scores,materialized_view. Worker routes task.type to the matching handler viathis.handlers[task.type]. MaterializedViews.refresh(name, computeFn)requires a compute function — the materialized_view handler passesasync () => []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 contextsfails (unrelated to Phase 4b). Time/day matching triggers on captures within the same second.
3-tier compaction (classify → cluster → consolidate → compress → archive → delete) that reduces active storage by ~79% at scale while improving recall quality.
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
| 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 |
| 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 |
{
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
}| 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 |
The LLM summarization is protected by 4 post-processing checks:
- Fabrication check — verifies key terms in the summary appear in source episodes
- Instruction leakage detection — strips any text matching the system prompt structure
- Contradiction preservation — ensures source contradictions are noted in the summary
- 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.
- 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. CompactionPipelinecan run individual steps —classifyOnly(),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.gzfile. 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 contextsfails (unrelated to Phase 4c).
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 RustLSH → RustLsh 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) |
cd packages/memory-core-rs
npx napi build --platform --release
# Produces: memory-core-rs.{platform}.node (~800KB darwin-arm64)- napi-rs naming: Exported class name
RustLsh(capital L, lowercase sh) — napi-rs capitalizes first letter of class names. The JS name isRustLsh, notRustLSH. - NAPI-RS Vec requires JS
Array<number>— NOTFloat64Array. ThenativeKMeans()andnativeBatchSimilarity()wrappers convertFloat64ArraytoArray<number>before calling into Rust. - Native addon is optional — all callers check
getNative()for null and fall back to TypeScript. No crash if.nodefile is missing. index.d.tsis manually maintained —napi build --no-dtswas used. Type declarations must be kept in sync withsrc/compute.rsandsrc/lsh.rs.streamInferencestreams viaAsyncTask+ThreadsafeFunction<String>— signature isstreamInference(modelPath, prompt, maxTokens, temperature, onToken) => Promise<string>. It shells out tollama-cli(if installed); without llama-cli it resolvesfinish_reason: "error"JSON. Do NOT change it back to a sync string return.get_embeddingis 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_pathreads real GGUF headers viasrc/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
RustLshinstance. jaccardSimilarityis NOT in the Rust addon — always uses the TypeScript implementation instorage.ts.napi.targetsmust stay in sync with the index.js loader —packages/memory-core-rsdeclares 8 triples (darwin x64/arm64, linux-gnu x64/arm64, linux-musl x64/arm64, win32-msvc x64/arm64). Do NOT usenapi.triples.defaults(it expands to only 4 targets in @napi-rs/cli v3). Usenapi.binaryName(not deprecatednapi.name).- The loader is no-throw —
index.jsexportsnull(not a throw) when no.nodebinary exists for the current platform, sogetNative()degrades to TypeScript. SetTIMPS_NATIVE_VERBOSE=1to log why the addon didn't load.
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 |
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
| 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 |
global:{normalizedQuery} (no org scope)
{orgId}:{projectId}:{normalizedQuery} (with org scope)
{orgId}:{projectId}:{normalizedQuery}:{suffix} (with suffix)
forge:{orgId}:{projectId}:{forgeName}:{stateType} (forge state)
CacheManager.invalidatePattern()prepends its own keyPrefix —CascadeCache.invalidateProject()passes the raw scope prefix (org:proj:) without acache:*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()returnsPromise<void>— not an unsubscribe function. Store the handler reference for later cleanup viaunsubscribe().- Cache warmup —
MemoryEngine.warmupCache()callsCascadeCache.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
MemoryEngineconstructor. It filters byprojectIdto avoid invalidating irrelevant projects. Cleaned up indispose().
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 |
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) │ │
- 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 (
spawnwithnode proxy-entry.js --stdio) — isolation, independent restart - Contradiction on
didChange(debounced 2s), bug pattern ondidSave— saves CPU forwardToRealServerWaitrejects immediately when no server — avoids 10s timeout- MemoryClient is an interface — enables testing without running MemoryServer
- 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.tserrors remain (3xseedEngineWithDatasetproperty missing) — unrelated to LSP work - LSP proxy uses
fetchfor MemoryClient HTTP calls — requires Node.js 18+ (built-infetchavailable) LspLocationLinktype — Added toprotocol.tsexports. Definition handler casts results with'uri' in loctype-narrowing check.- Test mock LSP server — inline Node.js script (
lspMockScript) provides minimal LSP responses, avoids real language server binaries process.execPathused for spawn in tests — avoidsENOENTerrors fromwhich 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
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 MemoryEngine — store, 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 |
@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)
- 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
{
"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"]
}npm install @timps-ai/timps-sdkimport { 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 }]- 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, BunBun.file, DenoDeno.readTextFile— all handled transparently @timps/memory-coreis an optional peer dependency — the SDK can work without it when bundled (imports are type-level where possible)- Tree-shaking — with
sideEffects: falseand 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 shutdownstore()is synchronous for local storage — embedding computation is fire-and-forget, BM25 handles immediate recall- SDK shares the canonical memory store —
createMemory()writes to~/.timps/memory/<projectHash>viamemoryDir(projectPath), the same location the CLI and memory dashboard read from. Memories written by the SDK are visible totimpsand the dashboard, and vice versa. An explicitMemoryOptions.diropts 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.walfiles and never overwrites existing canonical data; it is skipped entirely when an explicitdiris provided. (M70)
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 |
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
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
}]
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
- All timestamps stored as UTC ms since epoch (
Date.now()) - Display formatting:
Intl.DateTimeFormatwith 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
- Platform is metadata, not scope —
OrgScopedefines the isolation boundary (org + team + project). Platform (wechat,slack,discord) is stored onMemoryEntry.tagsand 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 recordsactorId,op,entryId, andpayloadfor every store operation. Audit just queries this log.- Team digest filters by
typeandactorId— not by full-text search. A digest is a summary of recent team activity, not a recall query. Userecall()for full-text search andgetTeamDigest()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 synchronous —
commit()checks the current head against the new commit's sentiment direction. If they appear conflicting, the commit is still stored butcrdtStatus: 'conflict_pending'is set on the branch metadata. CalllistBranches()withshowConflicts: trueto surface pending conflicts.
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.
| 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() |
In timps-code/src/core/agent.ts:
formatForSystemPrompt()called during system prompt constructionpreflightCheck()called before each user message executionrecordMistake()called on tool execution failure (isError=true)
Mistakes stored as individual JSON files (m_<timestamp>_<random>.json) in ~/.timps/learning/<projectHash>/.
| 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 |
- File-based persistence — mistakes are individual JSON files, not stored via MemoryEngine. Each file is atomic (write + rename).
- Similar mistake dedup —
recordMistake()incrementsoccurrenceswhenjaccardSimilarity(desc, existing) > 0.8instead of creating a new record. - Preflight warnings have a limit —
preflightCheck()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-compatible —
generateTrainingData()produces OpenAI-style messages format for fine-tuning pipelines.
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.
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
| 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 |
| 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 |
| 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 |
- Ollama is optional —
GemmaEmbeddergracefully degrades to deterministicMath.sin/Math.cosfallback embeddings when Ollama is unavailable. LocalVectorStoreuses JSONL — stored at<memoryDir>/multimodal.jsonl, no external vector DB required.- Cross-modal search —
crossModalRecall()searches all modalities separately, then expands results viagetLinkedEntries(). Linking is manual (linkEntries()). - Storage budget — default 100MB.
enforceStorageBudget()prunes least-accessed entries when over budget. - S3 integration —
ObjectStoragesupports opt-in S3 via@aws-sdk/client-s3(lazy-loaded). Default is local~/.timps/objects/withindex.jsontracking. - Screenshot capture — uses
screencapture(macOS) orimport(Linux, ImageMagick). Requires ImageMagick on Linux.
Latest commit (acf3383) fixed these issues alongside the new features:
timps-mcp/src/index.ts— Addedawaitto 5+ asyncrecall()callstimps-code/src/memory/memory.ts— Delegated branch ops toMemoryBranchStoreinstead of nonexistentMemoryEnginemethodspackages/memory-core/src/index.ts— AddedFileBackend,InMemoryBackend,seedEngineWithDatasetexportspackages/sdk/src/MemoryClient.ts— FixedFileBackendimport,EmbeddingConfigfields, null-safetytimps-code/src/plugins/pluginManager.ts— Fixed implicitanytype errorinstall.sh— Switched fromcd + npm run buildtonpm run build --workspace=timps-code
- Server deps already fixed —
packages/server/package.jsonalready has@timps-ai/memory-coreindependenciesandtypescriptindevDependencies. The AGENTS.md notes about missing deps and eval import errors inexecutor.tsare stale — all exports resolve correctly. supply-chain-audit.yml— already usesgoogle/osv-scanner-action@v2.3.8(upgraded from v1.0.2).