@jeffober/thatch 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/artifacts/skills/thatch-code-review.md +9 -7
- package/artifacts/skills/thatch-review-economy.md +97 -0
- package/artifacts/skills/thatch-review-followup.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +15 -5
- package/bin/qa-run +38 -0
- package/bin/release +21 -0
- package/bin/thatch +17 -5
- package/docs/dev/README.md +71 -18
- package/docs/dev/features/README.md +33 -0
- package/docs/dev/features/behavior-engine.md +271 -0
- package/docs/dev/features/cicd.md +89 -0
- package/docs/dev/features/cli.md +81 -0
- package/docs/dev/features/compaction-recovery.md +48 -0
- package/docs/dev/features/database.md +244 -0
- package/docs/dev/features/deduplication.md +102 -0
- package/docs/dev/features/extraction.md +253 -0
- package/docs/dev/features/hygiene.md +81 -0
- package/docs/dev/features/memory-store.md +154 -0
- package/docs/dev/features/multi-host.md +228 -0
- package/docs/dev/features/nudge-pipeline.md +282 -0
- package/docs/dev/features/prediction-engine.md +253 -0
- package/docs/dev/features/qa-system.md +205 -0
- package/docs/dev/features/session-lifecycle.md +110 -0
- package/docs/dev/features/setup.md +199 -0
- package/docs/dev/features/sideband.md +104 -0
- package/docs/dev/gotchas.md +3 -3
- package/docs/dev/mcp-parity.md +6 -4
- package/docs/dev/setup-and-hooks.md +4 -4
- package/docs/dev/skills.md +10 -8
- package/docs/plans/README.md +2 -2
- package/docs/user/README.md +58 -20
- package/docs/user/behavior-engine.md +85 -0
- package/docs/user/cli.md +98 -0
- package/docs/user/code-review.md +73 -0
- package/docs/user/deduplication.md +70 -0
- package/docs/user/default-behaviors.md +75 -0
- package/docs/user/extraction.md +74 -0
- package/docs/user/hygiene.md +76 -0
- package/docs/user/memory.md +81 -0
- package/docs/user/prediction-engine.md +87 -0
- package/docs/user/setup.md +110 -0
- package/docs/user/skills.md +97 -0
- package/package.json +2 -2
- package/src/behavior.ts +120 -0
- package/src/db.ts +160 -417
- package/src/embeddings.ts +0 -41
- package/src/extraction.ts +2 -2
- package/src/git.ts +3 -1
- package/src/index.ts +55 -9
- package/src/mcp.ts +30 -1
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +135 -188
- package/src/scoring-engine.ts +374 -0
- package/src/seed-behaviors.ts +339 -0
- package/src/setup.ts +2 -6
- package/src/sideband.ts +36 -8
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +172 -25
- package/src/vector-math.ts +30 -0
- package/docs/qa/README.md +0 -175
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Memory Store
|
|
2
|
+
|
|
3
|
+
The memory store is thatch's core feature: a SQLite-backed store of memories with semantic search via local embeddings. Everything else thatch does — extraction, hygiene, nudges, predictions, behaviors — reads from or writes to this store.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The store persists memories keyed by label, scoped to a project store or the shared "global" store. Each memory is a labeled chunk of text an agent writes for a future session of itself to read.
|
|
8
|
+
|
|
9
|
+
- Stores memories keyed by label within a store. The store name is auto-detected from the git remote — the `owner/repo` slug — or falls back to `"global"` for cross-project knowledge.
|
|
10
|
+
- Semantic search via cosine similarity over BGE-small-en-v1.5 embeddings (384 dimensions, ~34 MB). The model runs locally and is lazy-loaded.
|
|
11
|
+
- Six tools: `memory_remember`, `memory_recall`, `memory_list`, `memory_show`, `memory_forget`, and `store_list`. All are defined in `src/tool-defs.ts`.
|
|
12
|
+
- Branch scoping. Memories can be scoped to a git branch for work-in-progress context, or left unscoped for project-wide knowledge. Branch-scoped memories are recalled alongside project-wide ones when the branch matches.
|
|
13
|
+
- Confidence grading on a 1–10 scale, stored per memory.
|
|
14
|
+
- Archived flag. Archived memories are long-term records excluded from search and hygiene by default. Pass `includeArchived: true` to surface them.
|
|
15
|
+
- Write-time duplicate detection. When a new memory is similar to existing ones (cosine ≥ 0.85), the save returns a warning listing the near-duplicates. The save always proceeds — the warning asks the agent to reconcile, never blocks.
|
|
16
|
+
- Asymmetric search. Query text gets a BGE-specific prefix for retrieval; stored passages get no prefix. This follows the BGE training convention for asymmetric search.
|
|
17
|
+
- Multi-model dimension check. Entries embedded by a model with a different vector dimension are silently skipped during search, not ranked. The `model` column is informational — dimension is the discriminator.
|
|
18
|
+
- Recall tracking. Each memory carries `recall_count` and `last_recalled_at`, stamped when an agent-initiated search returns it. The hygiene system uses these to detect stale memories.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
### Store detection
|
|
23
|
+
|
|
24
|
+
`src/git.ts` parses `owner/repo` from the git remote URL. The resolution chain is worktree-safe:
|
|
25
|
+
|
|
26
|
+
1. Parse `owner/repo` from `git remote get-url origin`.
|
|
27
|
+
2. Fall back to the basename of the git common dir (worktree-safe).
|
|
28
|
+
3. Fall back to the directory basename.
|
|
29
|
+
4. Falls back to `"unknown"` if none of the above yield a result.
|
|
30
|
+
|
|
31
|
+
The "global" store always exists — it is inserted at schema init (`INSERT OR IGNORE INTO stores (name) VALUES ('global')`). Store creation is implicit: the first `memory_remember` to a new store calls `ensureStore`, which creates it via `INSERT OR IGNORE`.
|
|
32
|
+
|
|
33
|
+
### Embedding model
|
|
34
|
+
|
|
35
|
+
`src/embeddings.ts` defines the `BgeEmbeddingModel` class.
|
|
36
|
+
|
|
37
|
+
- Model: `Xenova/bge-small-en-v1.5` — 384 dimensions, ~34 MB.
|
|
38
|
+
- Lazy-loaded via the `@huggingface/transformers` `pipeline("feature-extraction")` API. The first load downloads from Hugging Face Hub; subsequent loads use the cached model.
|
|
39
|
+
- Query prefix: `"Represent this sentence for searching relevant passages: "` — the BGE asymmetric search convention. Queries get the prefix; passages do not.
|
|
40
|
+
- Mean pooling and L2 normalization are applied via pipeline options.
|
|
41
|
+
- Output is a `Float32Array`.
|
|
42
|
+
- `MockEmbeddingModel` is available for tests.
|
|
43
|
+
- The model can be overridden via the `THATCH_MODEL` environment variable.
|
|
44
|
+
|
|
45
|
+
### Write path (memory_remember)
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
agent calls memory_remember(label, content, opts)
|
|
49
|
+
→ model.passageEmbed("# label\n\ncontent") → Float32Array
|
|
50
|
+
→ db.findSimilar(store, embedding, { excludeSlug }) → write-time collision check
|
|
51
|
+
→ db.remember(store, label, content, embedding, model.name, opts)
|
|
52
|
+
overwrite:false → atomic INSERT (PK constraint rejects duplicates)
|
|
53
|
+
overwrite:true → upsert + clear stale dedup verdicts for that slug
|
|
54
|
+
→ return confirmation + warning if similar memories found
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The tool prepends `# ${label}\n\n` to the content before embedding. The stored `content` column holds this full formatted text — it is the exact input that was embedded, so a future re-embed requires no reconstruction.
|
|
58
|
+
|
|
59
|
+
The `findSimilar` call is a write-time collision check with no telemetry. It excludes the slug being written (self-exclusion on overwrite). If it finds entries with cosine ≥ 0.85, the return value includes a warning listing them.
|
|
60
|
+
|
|
61
|
+
The save always proceeds. The warning asks the agent to reconcile — merge into one entry (overwrite + forget the other) or record that they are genuinely distinct via `dedup_mark_checked`.
|
|
62
|
+
|
|
63
|
+
On overwrite, the upsert clears stale dedup verdicts: any `dedup_pairs` rows referencing that slug are deleted, since the content change may resolve or create duplicates.
|
|
64
|
+
|
|
65
|
+
### Read path (memory_recall)
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
agent calls memory_recall(query, opts)
|
|
69
|
+
→ model.queryEmbed(query) → Float32Array
|
|
70
|
+
→ db.recall([repo, "global"], queryEmbedding, { branch?, limit })
|
|
71
|
+
→ db.search() → cosine scoring, sort desc, top-N
|
|
72
|
+
→ stamp recall_count + last_recalled_at on returned rows
|
|
73
|
+
→ return formatted results with scores
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The tool layer hardcodes `[repo, "global"]` as the default search scope. Omitting the `store` parameter searches both. Passing a specific store searches only that store.
|
|
77
|
+
|
|
78
|
+
Entries with a mismatched embedding dimension are skipped during search — the `emb.length !== queryEmbedding.length` guard in `db.search()`. This handles the case where a different embedding model was used for older entries.
|
|
79
|
+
|
|
80
|
+
### Search vs. recall
|
|
81
|
+
|
|
82
|
+
Two methods, one scoring engine, different telemetry policy:
|
|
83
|
+
|
|
84
|
+
| Method | Telemetry | Used by |
|
|
85
|
+
|--------|-----------|---------|
|
|
86
|
+
| `db.search()` | None | Prompt-aware recall nudge (see [nudge-pipeline.md](nudge-pipeline.md)) |
|
|
87
|
+
| `db.recall()` | Stamps `recall_count` / `last_recalled_at` | `thatch_memory_recall` tool, CLI `thatch search` |
|
|
88
|
+
|
|
89
|
+
The distinction is deliberate. The recall nudge checks whether memories relate to a prompt — the plugin is looking, not the agent. Stamping telemetry on a nudge would inflate the "used recently" signal that hygiene reporting depends on. Only explicit agent-initiated recall stamps telemetry.
|
|
90
|
+
|
|
91
|
+
### Default recall scope
|
|
92
|
+
|
|
93
|
+
The tool layer hardcodes `[repo, "global"]` as the default search scope. This means a `memory_recall` call with no `store` argument searches both the project store and the global store. There is no way to search only the project store without the global store via the tool — pass an explicit `store` argument to scope to one.
|
|
94
|
+
|
|
95
|
+
### Archived memories
|
|
96
|
+
|
|
97
|
+
The `archived` column is an integer flag: `0` (default) is live, `1` is archived.
|
|
98
|
+
|
|
99
|
+
- `search()`, `findDuplicates()`, and the stale-entry count in hygiene all filter `WHERE archived = 0`.
|
|
100
|
+
- To search archived memories: pass `includeArchived: true` to `memory_recall`.
|
|
101
|
+
- To archive: write with `archived: true`. To unarchive: write with `archived: false`.
|
|
102
|
+
- Updating an already-archived memory requires an explicit `archived` parameter. The guard prevents accidental unarchival — the caller must consciously pass `archived: true` to keep it archived or `archived: false` to unarchive it.
|
|
103
|
+
|
|
104
|
+
### Slug generation
|
|
105
|
+
|
|
106
|
+
`slugify()` converts a label to a slug: lowercase, whitespace to hyphen, Unicode letters and digits preserved. All-symbol labels fall back to a hash so no label ever maps to an empty slug. ASCII labels produce the same slugs as earlier releases.
|
|
107
|
+
|
|
108
|
+
The slug plus store name form the composite primary key: `PRIMARY KEY (slug, store)`.
|
|
109
|
+
|
|
110
|
+
### CLI access
|
|
111
|
+
|
|
112
|
+
The `bin/thatch` CLI provides read-only access to the store:
|
|
113
|
+
|
|
114
|
+
| Command | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| `thatch stores` | List all stores |
|
|
117
|
+
| `thatch list [store]` | List memory labels in a store |
|
|
118
|
+
| `thatch show <label> [store]` | Display one memory |
|
|
119
|
+
| `thatch forget <label> [store]` | Delete one memory |
|
|
120
|
+
| `thatch search <query> [store]` | Semantic cosine search (limit 10; `"all"` searches project + global) |
|
|
121
|
+
|
|
122
|
+
## Interactions with other features
|
|
123
|
+
|
|
124
|
+
- **Extraction pipeline** ([extraction.md](extraction.md)): the extraction sub-agent writes memories via `memory_remember`. The extraction buffer drains on any `memory_remember` call.
|
|
125
|
+
- **Deduplication** ([deduplication.md](deduplication.md)): `find_duplicates` scans stored memories for cosine-similar pairs. `dedup_mark_checked` records verdicts in the `dedup_pairs` table. Overwriting or forgetting a memory clears its dedup verdicts.
|
|
126
|
+
- **Hygiene** ([hygiene.md](hygiene.md)): staleness checks use `recall_count` and `last_recalled_at` to find memories nobody has read recently. Duplicate checks use cosine search. Orphaned-branch detection finds branch-scoped memories whose branch no longer exists.
|
|
127
|
+
- **Nudge pipeline** ([nudge-pipeline.md](nudge-pipeline.md)): the recall nudge uses `db.search()` — not `db.recall()` — to avoid inflating telemetry. The nudge fires when search results exceed the similarity threshold (default 0.55).
|
|
128
|
+
- **Prediction engine** ([prediction-engine.md](prediction-engine.md)) and **behavior engine** ([behavior-engine.md](behavior-engine.md)): these share the same embedding model but store vectors in separate tables (`prediction_matchers`, `prediction_predictions`, `behavior_matchers`, `behavior_behaviors`). They do not read from or write to the `entries` table.
|
|
129
|
+
|
|
130
|
+
## Source files
|
|
131
|
+
|
|
132
|
+
| File | Responsibility |
|
|
133
|
+
|------|---------------|
|
|
134
|
+
| `src/db.ts` | SQLite schema, CRUD for entries and stores, brute-force cosine search (`search` vs. `recall`), dedup verdict tracking, prediction and behavior tables |
|
|
135
|
+
| `src/embeddings.ts` | `BgeEmbeddingModel` (lazy-load, query/passage embed, mean pooling, L2 normalization) and `MockEmbeddingModel` for tests |
|
|
136
|
+
| `src/git.ts` | Detect repo identity (store name) from git remote; worktree-safe fallback chain |
|
|
137
|
+
| `src/tool-defs.ts` | Tool definitions — single source of truth for all tools, including the six memory tools |
|
|
138
|
+
| `bin/thatch` | CLI subcommands: `stores`, `list`, `show`, `forget`, `search` |
|
|
139
|
+
|
|
140
|
+
## Key invariants
|
|
141
|
+
|
|
142
|
+
1. **No global mutable state.** Every module accepts dependencies explicitly. `ThatchDB` takes a path; `BgeEmbeddingModel` takes an optional pipeline factory; tools receive a `ctx` with `db` and `model`.
|
|
143
|
+
|
|
144
|
+
2. **Embedding is a separate concern.** `db.ts` stores and retrieves BLOBs and compares vectors handed to it. It knows nothing about embedding models — it never calls `passageEmbed` or `queryEmbed`. The tool layer is the bridge: it embeds, then hands the vector to the DB.
|
|
145
|
+
|
|
146
|
+
3. **Embedding spaces are discriminated by vector dimension, not model tag.** The `model` column is written on every save and returned on every read, but no code path filters or branches on it. Dimension mismatch is the only guard. This means a same-dimension model switch produces silently wrong cosine scores, while a different-dimension switch produces silently skipped entries.
|
|
147
|
+
|
|
148
|
+
4. **Store creation is implicit.** The first `memory_remember` to a new store creates it. There is no explicit "create store" operation.
|
|
149
|
+
|
|
150
|
+
5. **Default recall scope is repo + global.** The tool layer hardcodes `[ctx.defaultStore, "global"]` when no `store` argument is provided. This is intentional — cross-project knowledge in the global store is always searched alongside project-specific memories.
|
|
151
|
+
|
|
152
|
+
6. **The write-time similarity warning never blocks the save.** `findSimilar` returns a warning, `remember` proceeds regardless. The warning asks the agent to reconcile after the fact.
|
|
153
|
+
|
|
154
|
+
7. **`search()` records no usage. `recall()` stamps telemetry. This is deliberate.** The recall nudge (plugin checking relevance) must not inflate the "used recently" signal. Only agent-initiated recall counts as actual usage.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Multi-Host Support
|
|
2
|
+
|
|
3
|
+
Thatch runs as both an opencode plugin and an MCP server for Claude Code and
|
|
4
|
+
Cursor. Three integration paths share a common core---the tool definitions in
|
|
5
|
+
`src/tool-defs.ts`---but differ in how they deliver system prompts, prefix tool
|
|
6
|
+
names, and handle session lifecycle events.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
Thatch supports three host agents from a single codebase:
|
|
11
|
+
|
|
12
|
+
- **opencode plugin**---in-process, direct event hooks, warm embedding model,
|
|
13
|
+
`thatch_` tool prefix
|
|
14
|
+
- **Claude Code MCP**---stdio MCP server, external CLI hooks,
|
|
15
|
+
`mcp__thatch__` tool prefix, system prompt in CLAUDE.md
|
|
16
|
+
- **Cursor MCP**---same stdio MCP server, `--json` hook output format,
|
|
17
|
+
`mcp__thatch__` tool prefix, system prompt in AGENTS.md
|
|
18
|
+
|
|
19
|
+
All three paths expose the same 18 tool definitions from `src/tool-defs.ts`.
|
|
20
|
+
The tools are host-agnostic: each takes args and a `CoreContext`
|
|
21
|
+
(`{ db, model, defaultStore }`) and returns a string. The host-specific
|
|
22
|
+
wrapping---prefixing, transport, session hooks---lives outside the core.
|
|
23
|
+
|
|
24
|
+
## The three paths
|
|
25
|
+
|
|
26
|
+
### opencode plugin (`src/index.ts`)
|
|
27
|
+
|
|
28
|
+
Runs inside opencode's Bun runtime as a loaded plugin. No setup command is
|
|
29
|
+
needed---opencode discovers the plugin from `opencode.json` and loads it at
|
|
30
|
+
startup.
|
|
31
|
+
|
|
32
|
+
**System prompt**: injected at runtime via the
|
|
33
|
+
`experimental.chat.system.transform` hook. The hook calls `systemPrompt(repo)`
|
|
34
|
+
and pushes the result into `output.system` every turn. No files are written.
|
|
35
|
+
The repo name is baked in at runtime, so the prompt always reflects the current
|
|
36
|
+
worktree.
|
|
37
|
+
|
|
38
|
+
**Tools**: registered through opencode's `tool()` wrapper in `src/tools.ts`,
|
|
39
|
+
which adds the `thatch_` prefix. The agent sees `thatch_memory_remember`,
|
|
40
|
+
`thatch_memory_recall`, etc.
|
|
41
|
+
|
|
42
|
+
**Skills**: installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init.
|
|
43
|
+
Both the shared skill array and the opencode-only skill array are installed.
|
|
44
|
+
See [../skills.md](../skills.md) for the skill system.
|
|
45
|
+
|
|
46
|
+
**Capabilities unique to this path**:
|
|
47
|
+
|
|
48
|
+
- Full access to plugin hooks: system prompt injection, session events, tool
|
|
49
|
+
buffering, compaction context
|
|
50
|
+
- TUI toast notifications---best-effort, silently ignored in headless mode
|
|
51
|
+
- Direct extraction via child sessions (see [session-lifecycle.md](session-lifecycle.md))
|
|
52
|
+
- Compaction recovery (see [compaction-recovery.md](compaction-recovery.md))
|
|
53
|
+
- Background sub-agents (experimental)---the code-review coordinator dispatches
|
|
54
|
+
specialist sub-agents through opencode's agent infrastructure
|
|
55
|
+
|
|
56
|
+
### Claude Code MCP server (`src/mcp.ts`)
|
|
57
|
+
|
|
58
|
+
Runs as a stdio JSON-RPC 2.0 process spawned by Claude Code. The MCP server
|
|
59
|
+
exposes tools via `tools/list` and `tools/call`. It is long-lived for the
|
|
60
|
+
session duration, keeping the embedding model warm in memory.
|
|
61
|
+
|
|
62
|
+
**System prompt**: static text appended to `CLAUDE.md` by
|
|
63
|
+
`thatch setup --claude`. The text is written between marker constants
|
|
64
|
+
(`THATCH_MARKER` / `THATCH_END_MARKER`) using the `appendBlock` helper in
|
|
65
|
+
`src/setup.ts`. The block is idempotent---re-running setup replaces the content
|
|
66
|
+
between the markers rather than duplicating it. Claude Code loads `CLAUDE.md`
|
|
67
|
+
at session start.
|
|
68
|
+
|
|
69
|
+
**Tools**: the MCP server exposes bare names (`memory_remember`,
|
|
70
|
+
`memory_recall`). Claude Code's MCP client applies the `mcp__thatch__` prefix,
|
|
71
|
+
so the agent sees `mcp__thatch__memory_remember`. Thatch itself does not apply
|
|
72
|
+
this prefix.
|
|
73
|
+
|
|
74
|
+
**Skills**: installed to `$CLAUDE_CONFIG_DIR/skills/`---shared skills only.
|
|
75
|
+
The code-review coordinator skill is opencode-only because it requires
|
|
76
|
+
sub-agent dispatch, which Claude Code does not support.
|
|
77
|
+
|
|
78
|
+
**Session behavior**: driven by external CLI hook processes (`bin/thatch`).
|
|
79
|
+
The hooks are registered in `.claude/settings.json` during setup. Each hook
|
|
80
|
+
spawns a short-lived `thatch` process that communicates with the long-lived MCP
|
|
81
|
+
server via the sideband socket (see [sideband.md](sideband.md)) for warm-model
|
|
82
|
+
access. This avoids loading the ~34 MB embedding model in every one-shot hook
|
|
83
|
+
process.
|
|
84
|
+
|
|
85
|
+
**Setup**: `thatch setup --claude` writes `.mcp.json` (server config),
|
|
86
|
+
`CLAUDE.md` (instructions block), `.claude/settings.json` (hooks), and
|
|
87
|
+
installs skills. See [setup.md](setup.md) for the setup system.
|
|
88
|
+
|
|
89
|
+
### Cursor MCP server (same `src/mcp.ts`)
|
|
90
|
+
|
|
91
|
+
The same stdio MCP server as Claude Code. The differences are in hook format,
|
|
92
|
+
system prompt file, and tool batching granularity.
|
|
93
|
+
|
|
94
|
+
**System prompt**: static text appended to `AGENTS.md` by
|
|
95
|
+
`thatch setup --cursor`, using the same `appendBlock` helper and marker pair.
|
|
96
|
+
Cursor loads `AGENTS.md` at session start.
|
|
97
|
+
|
|
98
|
+
**Tools**: same bare names over MCP, same `mcp__thatch__` prefix applied by
|
|
99
|
+
Cursor's MCP client.
|
|
100
|
+
|
|
101
|
+
**Skills**: installed to `$CURSOR_CONFIG_DIR/skills/`---shared skills only.
|
|
102
|
+
|
|
103
|
+
**Session behavior**: driven by Cursor hooks in flat `hooks.json` format
|
|
104
|
+
(`.cursor/hooks.json`). Hook output uses the `--json` flag, producing
|
|
105
|
+
`{ additional_context: "..." }` objects that Cursor injects into the agent's
|
|
106
|
+
context. The `postToolUse` hook fires per-tool---there is no batch equivalent
|
|
107
|
+
of Claude Code's `PostToolBatch`. Tool buffering uses `thatch buffer-tool` on
|
|
108
|
+
each individual tool call. Cursor uses `conversation_id` instead of
|
|
109
|
+
`session_id` for session tracking.
|
|
110
|
+
|
|
111
|
+
**Setup**: `thatch setup --cursor` writes `.cursor/mcp.json`, `AGENTS.md`,
|
|
112
|
+
`.cursor/hooks.json`, and installs skills.
|
|
113
|
+
|
|
114
|
+
## System prompt delivery
|
|
115
|
+
|
|
116
|
+
Three mechanisms, one per host. The prompt functions all live in
|
|
117
|
+
`src/prompts.ts`.
|
|
118
|
+
|
|
119
|
+
1. **opencode**: the `experimental.chat.system.transform` hook calls
|
|
120
|
+
`systemPrompt(repo)` and pushes the result into `output.system` at runtime.
|
|
121
|
+
No files are written. The prompt is generated fresh every turn.
|
|
122
|
+
|
|
123
|
+
2. **Claude Code**: `claudeInstructions()` produces the text that
|
|
124
|
+
`thatch setup --claude` appends to `CLAUDE.md` between
|
|
125
|
+
`THATCH_MARKER` / `THATCH_END_MARKER`. The file is loaded once at session
|
|
126
|
+
start by Claude Code.
|
|
127
|
+
|
|
128
|
+
3. **Cursor**: `cursorInstructions()` produces the text that
|
|
129
|
+
`thatch setup --cursor` appends to `AGENTS.md` between the same markers.
|
|
130
|
+
Loaded once at session start by Cursor.
|
|
131
|
+
|
|
132
|
+
The three prompt functions are near-identical. They differ only in host name
|
|
133
|
+
string, config file reference (`OPENCODE.md` / `CLAUDE.md` / `AGENTS.md`), and
|
|
134
|
+
tool name prefix in the tool list line. Their shared prose sections---"When to
|
|
135
|
+
Write", "What NOT to Store", "Before Responding", "Stores", "Skills"---are
|
|
136
|
+
verbatim copies with these token substitutions.
|
|
137
|
+
|
|
138
|
+
There is no single-source-of-truth template that generates all three. Each
|
|
139
|
+
variant is an independent string constant in `src/prompts.ts`. Editing shared
|
|
140
|
+
prose in one variant requires mirroring the edit in the other two, or the
|
|
141
|
+
variants silently drift.
|
|
142
|
+
|
|
143
|
+
The tool list line in all three prompts must be kept in sync with
|
|
144
|
+
`TOOL_DEFS` in `src/tool-defs.ts`. The historical failure mode: a tool is added
|
|
145
|
+
to `TOOL_DEFS` but only two of the three prompt functions are updated, creating
|
|
146
|
+
an asymmetry a reviewer can catch by diffing the tool lines.
|
|
147
|
+
|
|
148
|
+
## Tool name prefixing
|
|
149
|
+
|
|
150
|
+
The 18 tool definitions in `src/tool-defs.ts` use bare names:
|
|
151
|
+
`memory_remember`, `memory_recall`, `memory_list`, etc. Each host applies its
|
|
152
|
+
own prefix.
|
|
153
|
+
|
|
154
|
+
| Host | Prefix | Applied by | Example |
|
|
155
|
+
|------|--------|------------|---------|
|
|
156
|
+
| opencode | `thatch_` | `tool()` wrapper in `src/tools.ts` | `thatch_memory_remember` |
|
|
157
|
+
| Claude Code | `mcp__thatch__` | Claude Code's MCP client | `mcp__thatch__memory_remember` |
|
|
158
|
+
| Cursor | `mcp__thatch__` | Cursor's MCP client | `mcp__thatch__memory_remember` |
|
|
159
|
+
|
|
160
|
+
The MCP server (`src/mcp.ts`) always exposes bare names. The
|
|
161
|
+
`mcp__thatch__` prefix is a convention of the MCP client, not of thatch.
|
|
162
|
+
The opencode prefix is applied in-process by the `tool()` wrapper.
|
|
163
|
+
|
|
164
|
+
## Feature availability differences
|
|
165
|
+
|
|
166
|
+
Not all features are available on all hosts. The opencode plugin has direct
|
|
167
|
+
access to session events, child sessions, and compaction hooks. The MCP hosts
|
|
168
|
+
rely on external hook processes and the sideband socket for equivalent
|
|
169
|
+
behavior, with some features having no MCP counterpart.
|
|
170
|
+
|
|
171
|
+
| Feature | opencode | Claude Code | Cursor |
|
|
172
|
+
|---------|----------|-------------|--------|
|
|
173
|
+
| Direct extraction (child sessions) | Yes | No | No |
|
|
174
|
+
| Compaction recovery | Yes | No | No |
|
|
175
|
+
| TUI toast notifications | Yes | No | No |
|
|
176
|
+
| Sideband socket | No (in-process) | Yes | Yes |
|
|
177
|
+
| code-review coordinator skill | Yes | No | No |
|
|
178
|
+
| Background sub-agents | Yes (experimental) | No | No |
|
|
179
|
+
| Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
|
|
180
|
+
|
|
181
|
+
For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
|
|
182
|
+
|
|
183
|
+
## Interactions with other features
|
|
184
|
+
|
|
185
|
+
All 18 tools are shared across all hosts via the single source of truth in
|
|
186
|
+
`src/tool-defs.ts`. Individual tool behavior is documented in
|
|
187
|
+
[memory-store.md](memory-store.md), [extraction.md](extraction.md),
|
|
188
|
+
[prediction-engine.md](prediction-engine.md),
|
|
189
|
+
[behavior-engine.md](behavior-engine.md), and
|
|
190
|
+
[deduplication.md](deduplication.md).
|
|
191
|
+
|
|
192
|
+
The **nudge pipeline** (see [nudge-pipeline.md](nudge-pipeline.md)) runs
|
|
193
|
+
in-process for opencode. For MCP hosts, it runs via the sideband socket---hook
|
|
194
|
+
processes query the long-lived MCP server for memory matches, predictions, and
|
|
195
|
+
behavior nudges through a Unix domain socket. See [sideband.md](sideband.md)
|
|
196
|
+
for the socket protocol.
|
|
197
|
+
|
|
198
|
+
The **extraction pipeline** (see [extraction.md](extraction.md)) uses an
|
|
199
|
+
in-memory ring buffer for opencode. For MCP hosts, it uses a file-backed JSONL
|
|
200
|
+
queue that hook processes append to and the MCP server drains.
|
|
201
|
+
|
|
202
|
+
The **setup system** (see [setup.md](setup.md)) handles Claude Code and Cursor
|
|
203
|
+
installation. opencode auto-installs skills and injects the system prompt at
|
|
204
|
+
plugin init---no setup command is needed.
|
|
205
|
+
|
|
206
|
+
**Skills** (see [../skills.md](../skills.md)) install both the shared and
|
|
207
|
+
opencode-only arrays for opencode. MCP hosts receive shared skills only, since
|
|
208
|
+
the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
209
|
+
|
|
210
|
+
## Source files
|
|
211
|
+
|
|
212
|
+
| File | Role |
|
|
213
|
+
|------|------|
|
|
214
|
+
| `src/index.ts` | opencode plugin entry---hooks, system prompt injection, session events |
|
|
215
|
+
| `src/mcp.ts` | MCP server (shared by Claude Code and Cursor)---stdio JSON-RPC, tool dispatch |
|
|
216
|
+
| `src/setup.ts` | Setup installer for Claude Code and Cursor---markers, hooks, skills |
|
|
217
|
+
| `src/prompts.ts` | All three system prompt variants---`systemPrompt()`, `claudeInstructions()`, `cursorInstructions()` |
|
|
218
|
+
| `src/tools.ts` | Thin opencode tool wrappers---imports tool-defs, adds `thatch_` prefix via `tool()` |
|
|
219
|
+
| `src/tool-defs.ts` | Single source of truth for all 18 tool definitions---name, description, zod schema, execute |
|
|
220
|
+
| `bin/thatch` | CLI hook commands for MCP hosts---`reminder`, `buffer-tool`, `flush-tools`, `setup` |
|
|
221
|
+
|
|
222
|
+
## Key invariants
|
|
223
|
+
|
|
224
|
+
- All three hosts share the same 18 tool definitions from `src/tool-defs.ts`. Adding a tool requires updating `TOOL_DEFS` and all three prompt functions.
|
|
225
|
+
- The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
|
|
226
|
+
- The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
|
|
227
|
+
- The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts`. The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
|
|
228
|
+
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts) have no MCP counterpart.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Per-Message Nudge Pipeline
|
|
2
|
+
|
|
3
|
+
Runs on every user message. Four priority tiers share one embedding
|
|
4
|
+
computation. The pipeline injects *synthetic parts*---text the model sees in
|
|
5
|
+
the conversation but the user does not see in the TUI. This is the inverse of
|
|
6
|
+
toast notifications, which are TUI-visible but model-invisible.
|
|
7
|
+
|
|
8
|
+
The nudge pipeline is thatch's core innovation. It is not a passive
|
|
9
|
+
store-and-retrieve system. It is a nudge layer that changes agent behavior by
|
|
10
|
+
injecting context at the right moment. See [extraction.md](extraction.md) for
|
|
11
|
+
the extraction nudge, [prediction-engine.md](prediction-engine.md) for the
|
|
12
|
+
prediction auto-fire, and [behavior-engine.md](behavior-engine.md) for the
|
|
13
|
+
behavior auto-fire.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
On every user message, thatch runs through four priority tiers:
|
|
18
|
+
|
|
19
|
+
1. **Extraction nudge**---if tool interactions are buffered, tell the agent to
|
|
20
|
+
extract memories.
|
|
21
|
+
2. **Recall nudge**---semantically search stored memories for context
|
|
22
|
+
relevant to the user's prompt.
|
|
23
|
+
3. **Prediction nudge**---surface user decision-model predictions relevant to
|
|
24
|
+
the current situation.
|
|
25
|
+
4. **Behavior nudge**---surface LLM self-discipline rules relevant to the
|
|
26
|
+
current situation.
|
|
27
|
+
|
|
28
|
+
Tiers 2--4 fire independently and share one embedding computation. Tier 1
|
|
29
|
+
returns early, skipping tiers 2--4.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
Two host paths deliver the same four tiers through different mechanisms. The
|
|
34
|
+
opencode path runs in-process with direct event hooks. The MCP path runs via
|
|
35
|
+
external CLI hook processes that communicate with the long-lived MCP server
|
|
36
|
+
through the sideband socket.
|
|
37
|
+
|
|
38
|
+
### opencode path (`src/index.ts`, `chat.message` hook)
|
|
39
|
+
|
|
40
|
+
#### Compaction guard
|
|
41
|
+
|
|
42
|
+
If the session is in the `compacting` set, thatch checks whether the incoming
|
|
43
|
+
message is a compaction summary. If it is, all nudges are suppressed---tools
|
|
44
|
+
are blocked during summary generation, so a nudge that says "call
|
|
45
|
+
`thatch_memory_recall`" would trigger a blocked-tool error. If the message is
|
|
46
|
+
not a compaction summary, compaction has already failed (the auto-continue
|
|
47
|
+
never fired). Thatch clears the stale `compacting` flag and proceeds normally.
|
|
48
|
+
See [compaction-recovery.md](compaction-recovery.md).
|
|
49
|
+
|
|
50
|
+
#### Tier 1---Extraction nudge (fallback path)
|
|
51
|
+
|
|
52
|
+
Fires only when direct extraction was never triggered or threw (the session is
|
|
53
|
+
not in the `extracting` set) **and** the in-memory buffer has pending
|
|
54
|
+
interactions.
|
|
55
|
+
|
|
56
|
+
- Injects `extractionNudge(count, missed, "thatch_memory_remember", sessionID)`
|
|
57
|
+
as a synthetic text part.
|
|
58
|
+
- Increments the `missedNudges` counter for escalation: polite at 0, insistent
|
|
59
|
+
at 2, ALL-CAPS shouting at 3+.
|
|
60
|
+
- Returns early. Tiers 2--4 are skipped.
|
|
61
|
+
|
|
62
|
+
See [extraction.md](extraction.md) for the full extraction pipeline, including
|
|
63
|
+
the accept/complete/requeue lifecycle and the `extracting` set.
|
|
64
|
+
|
|
65
|
+
#### Tier 2---Recall nudge
|
|
66
|
+
|
|
67
|
+
- Extracts user prompt text from non-synthetic text parts. Skips if the text
|
|
68
|
+
is shorter than `MIN_PROMPT_LEN` (10 characters, hardcoded). Trivially short
|
|
69
|
+
prompts like "yes" or "ok" match too broadly to be useful.
|
|
70
|
+
- Embeds the prompt with the warm in-process model: `model.queryEmbed(text)`.
|
|
71
|
+
- Searches memories via `db.search([repo, "global"], embedding, { limit: 5 })`.
|
|
72
|
+
This uses `db.search`, not `db.recall`, to avoid inflating recall telemetry.
|
|
73
|
+
The agent has not actually read the memories yet---the nudge only checks
|
|
74
|
+
whether they relate to the prompt.
|
|
75
|
+
- Filters results to `_score >= RECALL_THRESHOLD` (0.55, env:
|
|
76
|
+
`THATCH_RECALL_THRESHOLD`).
|
|
77
|
+
- If matches survive: injects `recallNudge(matches)` as a synthetic text part
|
|
78
|
+
and fires a toast notification via `client.tui.showToast`.
|
|
79
|
+
- Wrapped in its own try/catch. A recall failure does not block prediction or
|
|
80
|
+
behavior tiers.
|
|
81
|
+
|
|
82
|
+
#### Tier 3---Prediction auto-fire
|
|
83
|
+
|
|
84
|
+
- Reuses the same embedding from tier 2. No extra model call.
|
|
85
|
+
- Scores prediction matchers via
|
|
86
|
+
`db.scorePredictionNudge([repo, "global"], embedding, PREDICTION_THRESHOLD)`.
|
|
87
|
+
This is a mechanical query: cosine-match against matchers, then score linked
|
|
88
|
+
predictions by Bayesian confidence.
|
|
89
|
+
- Threshold: 0.60 (env: `THATCH_PREDICTION_THRESHOLD`). Higher than recall
|
|
90
|
+
because surfacing a preference nudge is more disruptive---the agent may act
|
|
91
|
+
on it or surface it to the user.
|
|
92
|
+
- If matches: injects `predictionNudge(items)` as a synthetic text part and
|
|
93
|
+
fires a toast.
|
|
94
|
+
- Independent try/catch.
|
|
95
|
+
|
|
96
|
+
See [prediction-engine.md](prediction-engine.md) for the confidence model,
|
|
97
|
+
matcher/prediction/edge data model, and the ambiguity-surfacing behavior.
|
|
98
|
+
|
|
99
|
+
#### Tier 4---Behavior auto-fire
|
|
100
|
+
|
|
101
|
+
- Reuses the same embedding. One more cosine scan, this time against
|
|
102
|
+
`behavior_matchers`.
|
|
103
|
+
- Scores via
|
|
104
|
+
`db.scoreBehaviorNudge([repo, "global"], embedding, BEHAVIOR_THRESHOLD)`.
|
|
105
|
+
- Threshold: 0.60 (env: `THATCH_BEHAVIOR_THRESHOLD`).
|
|
106
|
+
- If matches: injects `behaviorNudge(items)` as a synthetic text part and
|
|
107
|
+
fires a toast.
|
|
108
|
+
- Independent try/catch.
|
|
109
|
+
|
|
110
|
+
See [behavior-engine.md](behavior-engine.md) for the ham/spam feedback loop
|
|
111
|
+
and the self-discipline rule data model.
|
|
112
|
+
|
|
113
|
+
#### Tier independence
|
|
114
|
+
|
|
115
|
+
Tiers 2, 3, and 4 all fire independently. Any subset may inject. All share
|
|
116
|
+
one embedding computation. The embedding is computed once, after the
|
|
117
|
+
extraction tier returns early, and is reused across three cosine scans against
|
|
118
|
+
different tables.
|
|
119
|
+
|
|
120
|
+
### MCP path (`bin/thatch`, `flush-tools` subcommand)
|
|
121
|
+
|
|
122
|
+
The `flush-tools` subcommand fires on `UserPromptSubmit` (Claude Code) or
|
|
123
|
+
`beforeSubmitPrompt` (Cursor). It reads JSON from stdin:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"session_id": "...",
|
|
128
|
+
"conversation_id": "...",
|
|
129
|
+
"prompt": "...",
|
|
130
|
+
"message": "...",
|
|
131
|
+
"text": "...",
|
|
132
|
+
"user_input": "...",
|
|
133
|
+
"input": "..."
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Session ID is `session_id ?? conversation_id`. Prompt text is the first
|
|
138
|
+
present field from `prompt`, `message`, `text`, `user_input`, `input`.
|
|
139
|
+
|
|
140
|
+
Three priority tiers, first match wins:
|
|
141
|
+
|
|
142
|
+
#### Tier 1---Extraction nudge
|
|
143
|
+
|
|
144
|
+
- `peekQueue(sessionID)` peeks the file-backed JSONL queue without draining
|
|
145
|
+
it. The queue persists until the agent calls `memory_remember`, which
|
|
146
|
+
triggers `consumeQueue`.
|
|
147
|
+
- If interactions are pending: increments the missed count, prints
|
|
148
|
+
`extractionNudge(count, missed, "mcp__thatch__memory_remember", sessionID)`.
|
|
149
|
+
- Breaks. Tiers 2--3 are skipped.
|
|
150
|
+
|
|
151
|
+
See [extraction.md](extraction.md) for the file-backed queue and the
|
|
152
|
+
`buffer-batch` subcommand that populates it.
|
|
153
|
+
|
|
154
|
+
#### Tier 2---Recall + prediction + behavior via sideband
|
|
155
|
+
|
|
156
|
+
- Extracts prompt text. Skips if shorter than 10 characters.
|
|
157
|
+
- Connects to the sideband socket and fetches all three in parallel:
|
|
158
|
+
`sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`.
|
|
159
|
+
- Same thresholds as opencode: 0.55 recall, 0.60 prediction, 0.60 behavior.
|
|
160
|
+
- Concatenates any non-empty results: `claudeRecallNudge`,
|
|
161
|
+
`predictionNudge`, `behaviorNudge`.
|
|
162
|
+
- Prints to stdout. Breaks.
|
|
163
|
+
|
|
164
|
+
The sideband socket lets short-lived hook processes use the warm embedding
|
|
165
|
+
model in the long-lived MCP server, avoiding a cold ~34 MB model load on every
|
|
166
|
+
prompt. See [sideband.md](sideband.md) for the socket protocol.
|
|
167
|
+
|
|
168
|
+
#### Tier 3---Static write nudge
|
|
169
|
+
|
|
170
|
+
Fallback for conversational turns with no tool use and no memory matches.
|
|
171
|
+
Prints `claudeWriteNudge()`:
|
|
172
|
+
|
|
173
|
+
> After responding, check: did you learn new project knowledge, user
|
|
174
|
+
> preferences, or corrections worth persisting? If so, save to thatch.
|
|
175
|
+
|
|
176
|
+
On any error, falls back to the static write nudge. If that import also fails,
|
|
177
|
+
silent failure---the hook never blocks the user's prompt over an unrelated
|
|
178
|
+
error.
|
|
179
|
+
|
|
180
|
+
### `flush-predictions` subcommand
|
|
181
|
+
|
|
182
|
+
Standalone prediction fire---queries the sideband for scored predictions
|
|
183
|
+
only. Not installed as a hook. Exists for testing and for hosts that want
|
|
184
|
+
prediction-only output. `flush-tools` already fires predictions in tier 2.
|
|
185
|
+
|
|
186
|
+
### Output format
|
|
187
|
+
|
|
188
|
+
- **Claude Code**: plain text to stdout. Claude Code feeds hook stdout into
|
|
189
|
+
the agent's context.
|
|
190
|
+
- **Cursor**: `--json` flag wraps output as `{ additional_context: "..." }`.
|
|
191
|
+
Cursor injects `additional_context` into the session.
|
|
192
|
+
|
|
193
|
+
## Thresholds
|
|
194
|
+
|
|
195
|
+
All thresholds are env-overridable.
|
|
196
|
+
|
|
197
|
+
| Threshold | Default | Env var |
|
|
198
|
+
|-----------|---------|---------|
|
|
199
|
+
| Recall nudge | 0.55 | `THATCH_RECALL_THRESHOLD` |
|
|
200
|
+
| Prediction auto-fire | 0.60 | `THATCH_PREDICTION_THRESHOLD` |
|
|
201
|
+
| Behavior auto-fire | 0.60 | `THATCH_BEHAVIOR_THRESHOLD` |
|
|
202
|
+
| Min prompt length | 10 chars | hardcoded (`MIN_PROMPT_LEN`) |
|
|
203
|
+
|
|
204
|
+
The recall threshold (0.55) is lower than `findDuplicates`'s 0.85 because
|
|
205
|
+
"relates to" is a weaker signal than "duplicate." The prediction and behavior
|
|
206
|
+
thresholds (0.60) are higher than recall because surfacing a preference or
|
|
207
|
+
self-discipline rule is more disruptive than surfacing a memory---the agent
|
|
208
|
+
may act on it or interrupt the user.
|
|
209
|
+
|
|
210
|
+
## Toast notifications (opencode only)
|
|
211
|
+
|
|
212
|
+
`client.tui.showToast` is best-effort. If the TUI is not connected (headless
|
|
213
|
+
mode), the call is silently ignored. Toasts are model-invisible: they go to
|
|
214
|
+
the user only, not into the conversation history.
|
|
215
|
+
|
|
216
|
+
| Trigger | Message | Variant | Duration |
|
|
217
|
+
|---------|---------|---------|----------|
|
|
218
|
+
| Recall matches | recalled N memories | info | 3s |
|
|
219
|
+
| Prediction matches | N predictions surfaced | info | 3s |
|
|
220
|
+
| Behavior matches | N behaviors surfaced | info | 3s |
|
|
221
|
+
| Extraction child goes idle | new: N, updated: M, deleted: K | success | 4s |
|
|
222
|
+
|
|
223
|
+
The extraction metrics toast fires only when memories were actually written.
|
|
224
|
+
No-save runs produce no toast to avoid notification fatigue.
|
|
225
|
+
|
|
226
|
+
## Synthetic nudge parts (opencode only)
|
|
227
|
+
|
|
228
|
+
Nudges are injected via `client.session.prompt` with `noReply: true` and
|
|
229
|
+
`synthetic: true`. The `synthetic` flag is a TUI-visibility marker, not a
|
|
230
|
+
model-routing flag. The model sees the nudge text in the conversation. The
|
|
231
|
+
user does not see it in the TUI transcript. This asymmetry is what makes the
|
|
232
|
+
nudge-injection pattern work: the model can act on the nudge without
|
|
233
|
+
cluttering the user's view.
|
|
234
|
+
|
|
235
|
+
Do not confuse `synthetic` with `ignored`. A part marked `ignored` is dropped
|
|
236
|
+
from the LLM-bound message. A part marked `synthetic` is only hidden from the
|
|
237
|
+
TUI display. The opencode framework's history serializer filters on
|
|
238
|
+
`!part.ignored`, not `!part.synthetic`.
|
|
239
|
+
|
|
240
|
+
## Interactions with other features
|
|
241
|
+
|
|
242
|
+
- **Extraction pipeline** ([extraction.md](extraction.md)): tier 1 is the
|
|
243
|
+
extraction nudge. The `extracting` set suppresses tier 1 when direct
|
|
244
|
+
extraction is active.
|
|
245
|
+
- **Memory store** ([memory-store.md](memory-store.md)): tier 2 searches
|
|
246
|
+
stored memories via `db.search()`. This path records no telemetry---the
|
|
247
|
+
agent has not read the memories yet.
|
|
248
|
+
- **Prediction engine** ([prediction-engine.md](prediction-engine.md)): tier
|
|
249
|
+
3 scores prediction matchers by cosine, then scores linked predictions by
|
|
250
|
+
Bayesian confidence.
|
|
251
|
+
- **Behavior engine** ([behavior-engine.md](behavior-engine.md)): tier 4
|
|
252
|
+
scores behavior matchers by cosine, then scores linked behaviors by
|
|
253
|
+
ham/spam confidence.
|
|
254
|
+
- **Sideband IPC** ([sideband.md](sideband.md)): MCP hosts use the sideband
|
|
255
|
+
socket for warm-model access. The hook process sends the prompt text; the
|
|
256
|
+
MCP server embeds it and runs the cosine scans.
|
|
257
|
+
- **Compaction recovery** ([compaction-recovery.md](compaction-recovery.md)):
|
|
258
|
+
the nudge pipeline is suppressed during compaction. The compaction guard
|
|
259
|
+
clears stale flags when compaction fails.
|
|
260
|
+
- **Session lifecycle** ([session-lifecycle.md](session-lifecycle.md)):
|
|
261
|
+
direct extraction (`triggerExtraction`) suppresses tier 1 by adding the
|
|
262
|
+
session to the `extracting` set.
|
|
263
|
+
|
|
264
|
+
## Source files
|
|
265
|
+
|
|
266
|
+
| File | Role |
|
|
267
|
+
|------|------|
|
|
268
|
+
| `src/index.ts` | opencode: `chat.message` hook (all 4 tiers), compaction guard, extraction fallback |
|
|
269
|
+
| `bin/thatch` | MCP: `flush-tools` and `flush-predictions` subcommands |
|
|
270
|
+
| `src/sideband.ts` | MCP: sideband client helpers (`sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`) |
|
|
271
|
+
| `src/prompts.ts` | All nudge formatting functions (`recallNudge`, `claudeRecallNudge`, `predictionNudge`, `behaviorNudge`, `extractionNudge`, `claudeWriteNudge`) |
|
|
272
|
+
|
|
273
|
+
## Key invariants
|
|
274
|
+
|
|
275
|
+
- Tier 1 (extraction) returns early. Tiers 2-4 are skipped when tier 1 fires.
|
|
276
|
+
- Tiers 2-4 share one embedding computation. One embed, three cosine scans against different tables.
|
|
277
|
+
- Each tier is wrapped in its own try/catch. A failure in one tier does not block the others.
|
|
278
|
+
- The recall nudge uses `db.search()`, not `db.recall()`, to avoid inflating telemetry. The agent has not read the memories yet.
|
|
279
|
+
- The recall threshold (0.55) is lower than prediction/behavior (0.60) because "relates to" is weaker than "should surface to user."
|
|
280
|
+
- Nudges are suppressed during compaction. Tools are blocked during summary generation, so a nudge saying "call `memory_recall`" would trigger a blocked-tool error.
|
|
281
|
+
- `synthetic` means TUI-invisible, not model-invisible. The model sees the nudge text; the user does not. Do not confuse with `ignored`, which drops the part from the LLM-bound message.
|
|
282
|
+
- MCP host hooks must be silent on success. Only `flush-tools` prints. Any stdout from other hooks delays the agent loop.
|