Kucatoo-Code · Wiki

Memory, Workflows, and SkillDAG

Three subsystems surround the core query loop: the tiered memory subsystem (model_harness/memory/) stores and recalls facts across sessions, the workflow scheduler (model_harness/workflow/) runs multi-step DAG workflows on triggers, and the SkillDAG integration (model_harness/skilldag/) enriches prompts from — and mines skills into — an external skill-graph sidecar. All three are fail-open: none of them may break a query.

flowchart LR
    Q[User prompt] --> R[MemoryManager.recall]
    R --> STM[STM ring buffer]
    R --> MTM[MTM SQLite + embeddings]
    R --> LTM[LTM JSON files + Chroma index]
    STM --> MB[Relevant memory block]
    MTM --> MB
    LTM --> MB
    Q --> E[enrich_with_skilldag]
    E -->|GET search| SD[SkillDAG sidecar]
    SD --> SB[SkillDAG context block]
    MB --> SYS[system prompt sent to model]
    SB --> SYS
    SYS --> X[finished exchange]
    X -->|fire-and-forget| MINE[POST mine-skills]
    X -->|MemoryAwareAgent only| W[MemoryManager.remember]

Memory subsystem

Tiers

MemoryManager (model_harness/memory/manager.py:41) orchestrates three tiers, modeled on human cognition (model_harness/memory/dtos.py:24):

Tier Class Storage Eviction
STM STMStore (stm.py:30) in-memory OrderedDict ring buffer oldest evicted past 100 items / 10 000 estimated tokens (stm.py:38)
MTM MTMStore (mtm.py:38) SQLite at <DATA_DIR>/memory/mtm.db (WAL + busy timeout, mtm.py:69) none built in
LTM LTMStore (ltm.py:51) one JSON file per item under <DATA_DIR>/memory/ltm, plus a derived ChromaDB vector index at ltm/chroma when chromadb is importable (ltm.py:134) consolidate() prunes/merges

MemoryItem (dtos.py:41) carries content, importance (0–1), optional embedding, agent/session/user ids, and an access count. DATA_DIR is resolved in factory.py:208 (arg > DATA_DIR/HARNESS_DATA_DIR env > config default data); _wire_memory (factory.py:313) builds the stores, shares one EmbeddingProvider between MTM and LTM, and assigns the MemorySystems facade (system.py:20) to harness.memory_systems.

Write path

MemoryManager.remember() (manager.py:81) routes by tier argument; the default AUTO writes STM always, MTM always, and LTM only when importance >= promotion_threshold (0.7, manager.py:57). Cascade promotion also runs on access: record_access() increments the MTM access count and promotes MTM→LTM at 5 accesses (manager.py:359). LTMStore.write() additionally summarizes content over 5 000 tokens through the harness model when one is wired (ltm.py:270), and MemoryAwareAgent (memory_agent.py:23) wraps any agent to auto-store each task+result with a heuristic importance score after every run (memory_agent.py:87).

Recall path

MemoryManager.recall() (manager.py:161) searches all three tiers in parallel via asyncio.gather(..., return_exceptions=True), then merges by cross-tier rerank (2026-08-15): the stores' semantic paths attach a transient MemoryItem.score (cosine similarity), hits rank by raw score, keyword-tier hits take a mid-band default (0.55), ties break by tier preference STM > MTM > LTM (AC-4.17), and every tier contributes candidates before the top_k cut — so a strongly relevant LTM hit is no longer masked by weak STM/MTM matches. A tier that raises is logged and skipped, not fatal (manager.py:211). Per tier:

Embeddings engine

EmbeddingProvider (embeddings.py:219) tries engines in order and caches failures so a dead engine is not retried per call:

  1. local — sentence-transformers all-MiniLM-L6-v2, registered only if the package is importable (optional dependency, embeddings.py:255).
  2. remote — any OpenAI-compatible /embeddings endpoint, configured via HarnessConfig.embedding_* / OPENAI_API_KEY (embeddings.py:266).
  3. fallback — deterministic hash-based pseudo-embeddings that always work (embeddings.py:176).

Embedding cold-start is warmed in a background daemon thread at harness init (factory._wire_memoryEmbeddingProvider.warm_up(), 2026-08-15): the first embed in a process otherwise pays the torch import + model load (~90s worst case), which used to stall the first query after every restart. The local engine loads offline-first (local_files_only=True) when the model is cached, loads in the executor (never on the event loop), and every prompt section build is capped at 10s (fail-open skip) — a cold engine can no longer freeze streaming. When only the fallback is active, EmbeddingProvider.degraded is True and semantic recall is keyword-grade; GET /api/memory/stats surfaces embedding_degraded / embedding_provider (routes_memory.py:41, manager.py:467).

Fail-open design

Memory must never break a query. Concretely: recall exceptions per tier are swallowed (manager.py:211); a failed embed stores the item without a vector instead of a zero-vector (ltm.py:296); Chroma failures fall back to the in-memory index (ltm.py:162); the direct-chat and agent recall hooks are wrapped in bare try/except (routes_query.py:670, agents/base.py:1094); and LTMStore._load_from_disk skips corrupt JSON files with a warning (ltm.py:240).

Where memory enters a query

Workflow scheduler

Definitions

A workflow is a WorkflowDefinition5 (workflow/dtos.py:214): id, ordered WFStep list, error_handling (fail/continue/retry), and max_concurrency. Each WFStep (dtos.py:71) has a type — agent, tool, condition, parallel, human_gate, sub_workflow, transform, sleep, skill — plus depends_on, max_retries, Jinja2-templated args, and an output_mapping that copies step outputs into workflow variables. skill steps execute through the Phase 6 SkillLibrary (step_handlers.py:334).

Execution

WorkflowExecutor.execute() (executor.py:88) builds the DAG with DAGBuilder (dag.py:32) — duplicate step ids, missing dependencies, and cycles all raise DAGError — then Kahn's topological sort yields levels; steps within one level run in parallel via asyncio.gather (executor.py:213). Failures retry with exponential backoff (1s, 2s, 4s…, executor.py:301), then follow the workflow's error_handling. Human-gate steps pause for POST /api/workflows/approve (routes_workflows.py:42); a paused workflow records its next level and resume_workflow re-enters the loop there (executor.py:367). Executions are queryable via GET /api/workflows/executions.

Schedules

A Schedule (dtos.py:364) binds a registered workflow to one of four trigger types (TriggerType, dtos.py:58):

WorkflowSchedulerEngine.start() (scheduler.py:190) loads persisted schedules and ticks every second (asyncio.sleep(1)), firing due schedules as strongly-referenced background tasks; stop() awaits in-flight tasks. Each schedule is persisted as <DATA_DIR>/schedules/<id>.json for crash recovery, with the id validated against ^[A-Za-z0-9_-]+$ to block path traversal (scheduler.py:40). The facade WorkflowScheduler (system.py:49) exposes schedule(), add_interval_schedule(), add_webhook_schedule(), add_event_schedule(); the factory wires it with agent framework, tool system, and memory manager and starts the loop as a background task (factory.py:252).

SkillDAG sidecar

The SkillDAG integration talks to an external skill-graph server (default http://localhost:8000, run separately; its code is not in this repo). Maintenance mode since 2026-08-15 (MCP-Arc.md §10): A/B evals showed no pass-rate delta and model-visible skills now ship in-process (use_skill tool + catalog section), so the sidecar is kept fail-open but no longer invested in. Everything is dormant unless SKILLDAG_ENABLED=true (skilldag/config.py:48; other knobs: SKILLDAG_URL, SKILLDAG_READ_GRAPH, SKILLDAG_WRITE_GRAPH, SKILLDAG_TIMEOUT, SKILLDAG_ENRICH_ENABLED, SKILLDAG_MINE_ENABLED).

Client

SkillDagClient (skilldag/client.py:40) is stdlib-urllib only and never raises: every call returns a {ok, status, data, error} envelope, and a disabled config short-circuits before any network I/O (client.py:68). Three endpoints are used: GET /api/v1/health, GET /api/v1/graphs/{g}/search?q=&top_k=&depth=, and POST /api/v1/graphs/{g}/mine-skills. Mining gets a 60s timeout because it invokes an LLM server-side; search/health use SKILLDAG_TIMEOUT (5s) / 2s (client.py:34).

Read path: context-block injection

enrich_with_skilldag(query) (integration.py:59) searches the read graph and renders a [SkillDAG context] block listing matching skills (skill_id: description), Related skills via typed edges: neighbors, and Avoid combining: conflicts, capped at 200 chars per snippet and 4 000 chars total. It returns "" on any failure, so callers append unconditionally. Call sites: direct chat (routes_query.py:654, joined with the memory block into the system prompt at routes_query.py:687) and agent runs, once per run inside _think (agents/base.py:786).

Write path: skill mining from finished exchanges

After a completed exchange, schedule_mining(prompt, response) (integration.py:151) fires mine_skills_after_query off the request path — asyncio.to_thread when a loop runs, else a daemon thread — which POSTs the pair to /mine-skills on the write graph. The server extracts reusable skills and returns {new_skills, existing_skills, skipped, total_extracted} (client.py:152). Call sites: end of the direct stream (routes_query.py:818) and the agent stream (routes_query.py:1140), both using the original unenriched prompt. Gated by SKILLDAG_MINE_ENABLED; failures are logged at debug and swallowed.

Propose-then-commit graph

Graph structure changes on the sidecar are two-phase (verified in the sidecar repo at F:/Kucatoo/Sites/Kucatoo-Code/SkillDAG/dev/skilldag_server/, outside this repo): propose_edge / propose_remove_edge / propose_retype_edge are dry-run previews that validate acyclicity and contradictions and return {would, related} without mutating (core/graph.py:823); edit_edge is the only write path, re-validating, appending to history, and saving (core/graph.py:888). These are exposed as REST POST /graphs/{id}/edges/propose|edit and as MCP tools whose descriptions mandate propose-before-commit. The Kucatoo-Code client does not call these — it only searches and mines; edge curation is an operator/MCP concern on the server.

Scope notes

verified against code: 2026-08-15