Repository navigation
Expand file tree
/
Copy pathpalinode.config.yaml.example
More file actions
491 lines (463 loc) · 23.3 KB
/
Copy pathpalinode.config.yaml.example
File metadata and controls
491 lines (463 loc) · 23.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# =============================================================================
# Palinode Configuration
# =============================================================================
# Copy this file to `palinode.config.yaml` in your Palinode directory.
# All values shown are defaults — only override what you need.
# -----------------------------------------------------------------------------
# Memory Directory
# -----------------------------------------------------------------------------
# Where Palinode stores markdown memory files and the SQLite-vec index.
# All paths below are relative to this directory unless absolute.
#
# Leave `memory_dir` commented out to inherit the PALINODE_DIR environment
# variable — this is the recommended setup. The Quick Start exports
# `PALINODE_DIR=~/.palinode`, and PALINODE_DIR overrides `memory_dir` at load
# time; setting `memory_dir` here too only invites the two to disagree (files
# write to one directory while the DB attaches to another, and search silently
# returns nothing). Uncomment and set an absolute path only if you are NOT
# using PALINODE_DIR.
# memory_dir: "~/.palinode"
# SQLite-vec database location (relative to memory_dir)
db_path: ".palinode.db"
# -----------------------------------------------------------------------------
# Capture (mechanical cross-linking during indexing)
# -----------------------------------------------------------------------------
capture:
cross_refs:
enabled: true
# Floor below which a bare slug/title token is NOT matched in prose, to
# avoid false-positive substring hits on short names.
min_token_len: 6
# Mine harness session transcripts for user corrections — moments you
# overturned a decision, rejected an approach with a reason, or asked for
# something to be remembered. Every result is a PROPOSAL: nothing here writes,
# changes or retires a memory. See docs/HARNESSES.md.
#
# Three separate opt-ins, in this order. Each one is deliberate:
# 1. enabled — turn the source on
# 2. harness_paths — name the directories it may read
# 3. classify — allow it to ask a model, which is what sends text
transcripts:
# 1. Off by default. Nothing is read and nothing is written until this is true.
enabled: false
# 2. Which harnesses, and which directories. Empty by default: enabling the
# source with no paths still reads nothing. Absolute paths only; a path
# traversing a symlink is refused.
harness_paths: {}
# claude-code:
# - ~/.claude/projects
# 3. Classification is off by default, and it is the ONLY part that
# transmits anything. With it false, a scan runs the deterministic
# pattern stage locally, makes NO request to any model endpoint, and
# queues every candidate as `needs_review` for you to look at.
# With it true, each candidate's bounded window (at most 5 turns, 400
# characters each — your own turns and ordinary assistant replies only)
# is sent to the endpoint configured under `consolidation.llm_url`.
# IF THAT ENDPOINT IS NOT LOCAL, THAT TEXT LEAVES THIS MACHINE.
# `palinode controls status` reports the destination.
classify: false
# Bounds on a scan. Both report what they skipped rather than truncating
# quietly.
lookback_days: 7
max_candidates: 50
# -----------------------------------------------------------------------------
# Ingestion Pipeline
# -----------------------------------------------------------------------------
ingestion:
# Inbox directory for dropped files (relative to memory_dir)
inbox_dir: "inbox/raw"
processed_dir: "inbox/processed"
# Maximum characters to extract from PDFs
pdf_max_chars: 10000
# Maximum characters to extract from URLs
url_max_chars: 10000
# Transcriptor API for audio/video files
transcriptor:
url: "http://localhost:8787"
timeout_seconds: 600
# -----------------------------------------------------------------------------
# Embeddings
# -----------------------------------------------------------------------------
embeddings:
# Embedding backend (Ollama by default)
primary:
model: "bge-m3"
url: "http://localhost:11434"
dimensions: 1024
# Timeout for first call (model loading)
timeout_seconds: 120
connect_timeout_seconds: 10
# Wire protocol at `url`: "ollama" (default; native /api/embed with the
# /api/embeddings fallback) or "openai" (POST {model, input} to
# /v1/embeddings — llama.cpp `llama-server --embedding`, vLLM, LM Studio).
# No auto-detection: set it explicitly. Retry/backoff, the circuit breaker,
# and per-input error handling are the same either way. A `url` that
# already ends in /v1 is fine. Note the Ollama tag "bge-m3" is not a
# llama-server model name — point llama-server at a BGE-M3 GGUF instead.
# dialect: "ollama"
#
# Example — llama.cpp serving a BGE-M3 GGUF:
# llama-server -m bge-m3-Q8_0.gguf --embedding --pooling cls -c 8192 --port 8080
# dialect: "openai"
# url: "http://localhost:8080"
# model: "bge-m3" # llama-server ignores it; vLLM / LM Studio match it
#
# Example — hosted OpenAI-compatible embedding provider:
# Credentials are read from PALINODE_EMBEDDING_API_KEY, or from the file
# named by PALINODE_EMBEDDING_API_KEY_FILE. Do not put API keys in YAML.
# dialect: "openai"
# url: "https://generativelanguage.googleapis.com/v1beta/openai"
# endpoint_path: "/embeddings"
# model: "gemini-embedding-001"
#
# Ollama's GPU path returns a NaN vector for a few exact inputs (a flash-
# attention precision bug in llama.cpp on cacheless encoders; the same input
# embeds fine on CPU). Such a rejection is retried once with num_gpu 0 and
# keep_alive 0, so the model does not stay CPU-resident afterwards. Off →
# the chunk stays keyword-only until re-embedded. Ollama dialect only.
# nan_cpu_retry: true
# For an embedder slower than the save timeout (PALINODE_SAVE_TIMEOUT, 30 s): /save
# returns keyword-searchable, the watcher embeds. Env: PALINODE_DEFER_EMBED_ON_SAVE.
# defer_embed_on_save: false
# -----------------------------------------------------------------------------
# Auto-Summary
# -----------------------------------------------------------------------------
auto_summary:
enabled: true
# Model used for generating summaries (should be fast and small)
model: "qwen2.5:14b-instruct"
# Maximum summary length in characters
max_chars: 120
# Minimum content length to trigger summary generation
min_content_chars: 200
# Wire protocol for the CHAT *primary* (model @ ollama_url):
# "ollama" (default) — Ollama-native /api/generate.
# "openai" — an OpenAI-compatible /v1/chat/completions endpoint
# (LM Studio, vLLM, a shim). Use when the primary is e.g.
# an MLX model served by LM Studio. With "openai", ANY
# primary failure cascades to llm_fallbacks (a remote
# primary's connect drop is what backups exist for).
# api: "openai"
# Ollama URL for summary generation (uses embeddings.primary.url if not set).
# When api: "openai" this is the OpenAI-compat base URL of the primary.
# ollama_url: "http://localhost:11434"
# OpenAI-compat fallback chain for the CHAT role (auto-description/-summary),
# walked in order on primary failure. Only fires on the watcher-driven
# /generate-summaries backfill — never the /save hot path — so enrichment still
# lands during a primary outage without egressing on every save. Empty (the
# default) = no fallback. Each entry is an OpenAI-compat /v1/chat/completions
# endpoint. Example ladder (LM Studio MLX primary → local vLLM → Sonnet shim):
# api: "openai"
# model: "qwen3-coder-30b-a3b-instruct-dwq"
# ollama_url: "http://lmstudio-host:1234" # LM Studio (e.g. a Mac Studio)
# llm_fallbacks:
# - model: "qwen-coder" # local vLLM (fast, on-LAN)
# url: "http://vllm-host:8000"
# metered: false # free host: exempt from the cap below
# - model: "claude-sonnet-4-6" # label only; shim sets SHIM_MODEL
# url: "http://127.0.0.1:4010" # local Sonnet shim (last resort)
# Per-/generate-summaries-run cap on how many deferred files may escalate to the
# fallback chain, bounding egress (esp. to the Sonnet link) when the primary is
# down and one backfill spans a large backlog. 0 = unlimited. Only applies with
# llm_fallbacks, and only to entries that are metered (the default); an entry
# with `metered: false` never spends or is blocked by this budget.
# llm_fallback_max_per_run: 10
# -----------------------------------------------------------------------------
# Search Defaults
# -----------------------------------------------------------------------------
search:
# lexical: keyword recall and indexing with no embedding/chat service.
# See docs/lexical-retrieval.md for setup, diagnostics, and limitations.
# Restart after changing mode; run palinode reindex when enabling hybrid.
retrieval_mode: hybrid
# Hybrid search combines vector similarity with BM25 keyword matching.
# This catches exact terms (model names, IDs, code) that vectors miss.
hybrid_enabled: true
# Balance: 0.0 = vector only, 1.0 = BM25 only, 0.5 = equal weight
hybrid_weight: 0.5
# These two search floors were measured against real bge-m3 embeddings.
# Re-check them if you change the embedding model.
# Default threshold for MCP server searches (lower = more results)
mcp_threshold: 0.4
# Default threshold for FastAPI searches (higher = more precision)
api_threshold: 0.5
# BM25 arm floor, relative to the best keyword match in the same result set
# (a candidate survives when score >= fts_threshold * top_score). 0.0 = no floor.
fts_threshold: 0.4
# The same shape for the vector arm: a candidate survives when its cosine is
# at least this fraction of the best cosine in the same candidate set.
# mcp_threshold / api_threshold above are ABSOLUTE floors — they say whether
# a match is plausible at all, not whether it is plausible next to the match
# this query found, which is what let weak neighbours fill every slot.
# Relative to the best match, so the top result always survives: this bounds
# a slate, it never returns nothing. 0.0 = no relative floor.
vector_relative_floor: 0.85
# The same floor for searches where the keyword arm is the ONLY arm —
# retrieval_mode: lexical, and the keyword fallback a healthy embedder's
# per-input rejection falls back to. Looser than fts_threshold on purpose:
# there is no vector arm to re-admit a candidate the keyword arm dropped.
# Measured as the last value below the weakest true hit on the relevance rig.
# 0.0 = no floor, which fills `limit` whatever the evidence is.
lexical_fts_threshold: 0.35
# How many chunks of ONE source file may take slots ahead of other files'
# competitive chunks. Overflow is deferred rather than dropped: it still
# fills a slate that would otherwise come back short, so this changes which
# results you get and never how many. 0 = unlimited.
max_chunks_per_file: 1
# Every delivery reports a match-confidence verdict (confident/weak/none)
# read from the pre-fusion arm scores; see docs/lexical-retrieval.md. It is
# a signal about what was delivered and never decides what is delivered —
# the two floors above do that. Turning this on withholds the slate on the
# MCP surface when the verdict is `none` (nothing found clears even the low
# mark). Off by default: an empty slate cannot be told apart from an empty
# store. Other surfaces always return their rows.
abstain_on_no_confident_match: false
# Default number of results
default_limit: 15
# Status values to exclude from search results
exclude_status:
- "archived"
# -----------------------------------------------------------------------------
# Consolidation
# -----------------------------------------------------------------------------
# Three-tier memory freshness:
# Tier 1: Session append — free, every session exit (hook or MCP call).
# Appends summary to daily/{date}.md + one-liner to projects/{slug}-status.md.
# Tier 2: Nightly dedup — lightweight LLM pass over whatever has not been
# consolidated yet (a per-project watermark, not a window).
# UPDATE/SUPERSEDE only. No restructuring, no archival.
# Tier 3: Weekly deep clean — full compaction with MERGE/ARCHIVE.
# Restructures status files, archives processed daily notes.
#
# HOW TO INSTALL CRON JOBS:
# # Nightly — lightweight dedup of everything not yet consolidated (Tier 2)
# # No --days: the nightly's watermark decides what it reads. Passing
# # --days N still works and sets the catch-up bound (see nightly below).
# 0 11 * * * cd /path/to/palinode && PALINODE_DIR=/path/to/data \
# venv/bin/python -m palinode.consolidation.cron --nightly \
# >> /path/to/data/logs/consolidation.log 2>&1
#
# # Weekly — full compaction (Tier 3)
# 0 11 * * 0 cd /path/to/palinode && PALINODE_DIR=/path/to/data \
# venv/bin/python -m palinode.consolidation.cron --days 3 \
# >> /path/to/data/logs/consolidation.log 2>&1
#
# TIMING NOTES:
# - Times above are UTC. Adjust for your timezone (11:00 UTC = 4am PT).
# - Nightly runs daily; weekly runs Sundays. On Sundays both fire —
# nightly first (UPDATE/SUPERSEDE), then weekly (full ops). No conflict.
# - The `schedule` field below is informational only — actual scheduling
# is via crontab, systemd timers, or your preferred scheduler.
#
consolidation:
enabled: true
# Informational — actual schedule lives in crontab (see above)
schedule: "0 11 * * 0" # Sunday 4am PT (11:00 UTC)
# How many days of daily notes the weekly full pass reads. Keep this equal
# to the `--days N` in your crontab — the cron argument overrides this value
# silently, and `palinode doctor` reports the effective number if they differ.
lookback_days: 3
# LLM API for consolidation (any OpenAI-compatible endpoint)
llm_url: "http://localhost:8000" # vLLM, Ollama, etc.
# llm_model: model path or identifier for your LLM backend
# Examples:
# vLLM: "/model" (default path)
# Ollama: "llama3.1:8b" or "qwen2.5:14b-instruct"
llm_model: "/model"
llm_temperature: 0.3
llm_max_tokens: 2000
# Which operations the weekly full pass may apply. The nightly pass has
# its own, separately-scoped restriction below (nightly.allowed_ops) —
# this one only governs the weekly pass.
# PROPOSE_CONTRADICTS records a conflict between two memories without
# picking a winner (SUPERSEDE stays the only winner-picking op); drop it
# from this list to stop the pass proposing contradiction links at all.
# ARCHIVE_BEFORE retires every dated `- [YYYY-MM-DD] …` status log line
# older than one date in a single operation, so a proposal's size follows
# the number of reasons rather than the number of facts. Weekly only — it
# is deliberately absent from nightly.allowed_ops below.
allowed_ops: ["KEEP", "UPDATE", "MERGE", "SUPERSEDE", "ARCHIVE", "RETRACT", "PROPOSE_CONTRADICTS", "ARCHIVE_BEFORE"]
# How long a dated `- [YYYY-MM-DD] …` status log line stays in the document
# before the weekly pass retires it into the `-history.md` sibling — no LLM
# involved, applied before the model is shown anything. A status document
# fed by session-end gains a line per session; at several hundred, an honest
# per-line proposal overruns any token cap and nothing is ever retired.
# 90 days is a quarter, and more than twelve weekly passes. 0 disables the
# sweep. Identity/profile documents are never swept (ADR-020).
status_log_retention_days: 90
# Nightly pass configuration (Tier 2)
nightly:
enabled: true
# The nightly's CATCH-UP BOUND, not a window. Selection is a per-project
# watermark — the notes written since the last pass that resolved that
# project — so a healthy run reads exactly the new notes and nothing else,
# and a failed run's notes are selected again because a failure does not
# advance the mark. This number only bounds how far back a cold or
# long-failed mark may reach, so one abandoned project cannot hand the
# model months of notes in a single request; whatever the bound excludes
# is named in the log, never dropped silently.
# 7 matches auto_gate.max_hours_elapsed (168 h): the gate may let a week
# pass before firing at its ceiling, and a shorter bound would drop the
# notes the gate itself chose to wait on. `--days N` on the nightly cron
# line overrides it, with the same meaning.
lookback_days: 7
# Prompts one pass may send a project whose new notes do not fit one
# prompt (about 6,000 characters of notes each). Each prompt resumes where
# the last stopped; the pass stops early when nothing is pending or a
# prompt fails or is cut off, and the mark then stays at the end of the
# last prompt that resolved. Raising it clears more a night without making
# any single call bigger. 1 = one prompt per project per pass.
max_prompts_per_project: 4
# Restrict to safe, additive operations — no cross-date restructuring.
# MERGE is allowed but the executor enforces a same-day guard: only facts
# sharing the same [YYYY-MM-DD] date prefix may be merged in a nightly run.
# Cross-date or undated MERGE proposals are rejected with a log warning.
# PROPOSE_CONTRADICTS is additive in the same sense: it writes a
# `contradicts` link and retires nothing.
allowed_ops: ["UPDATE", "SUPERSEDE", "MERGE", "PROPOSE_CONTRADICTS"]
# Activity gate for the AUTOMATIC path only (`python -m
# palinode.consolidation.cron`). A pass runs when BOTH conditions hold:
# enough hours elapsed AND enough session-end entries recorded since that
# pass last ran. Wall-clock cron alone gets both cases wrong — an idle week
# still burns an LLM pass, a heavy day still waits for the next tick — so
# with the gate on you can schedule the cron hourly and let the gate decide.
#
# `max_hours_elapsed` is the ceiling that defeats the gate: past it the pass
# runs whatever the session count is. Keep it. Without it, a store that
# ingests through the watcher and records no sessions would never
# consolidate at all. The default equals the weekly cadence consolidation
# already had, so an idle deployment keeps today's behaviour.
#
# Weekly and nightly passes are gated independently (state lives in
# `<memory_dir>/.palinode/consolidation-state.json`).
#
# On-demand runs bypass the gate: `palinode consolidate`, `palinode dream`,
# POST /consolidate and the MCP tool run unconditionally unless they pass
# `--respect-gate` / `respect_gate: true`.
auto_gate:
enabled: true
min_hours_elapsed: 24
min_sessions: 5
max_hours_elapsed: 168 # 7 days — the ceiling, not the schedule
# Write-time contradiction check (Tier 2a — ADR-004)
# When enabled, every palinode_save schedules a background contradiction check
# against the top-k most similar existing memories. Validated updates are
# applied in a separate commit.
# Runs asynchronously — never blocks the save caller.
#
# Default: disabled. Flip to true after validating in a dev environment.
# Sync opt-in: set query param ?sync=true on /save or pass --sync to CLI.
write_time:
enabled: false
queue_max_size: 1000
check_timeout_seconds: 30
pending_dir: ".palinode/pending"
sweep_on_startup: true
# How many times a pending marker may be handed to the worker before it
# is retired to .failed.json for operator review.
max_attempts: 3
# Optional: map project entity refs to keywords for auto-grouping
# daily notes that don't contain explicit `project/foo` entity refs.
# keyword_map:
# "project/my-app": ["my-app", "MyApp", "the app"]
# -----------------------------------------------------------------------------
# Compaction
# -----------------------------------------------------------------------------
compaction:
# Layer split heuristics — these keywords classify ## section headings
# into Identity (slow-changing) vs Status (fast-changing) layers.
#
# HOW THESE WERE DETERMINED:
# Initial guesses based on common markdown heading conventions.
# Not scientifically derived — tune them to match your actual file structure.
#
# HOW TO EVOLVE THEM:
# 1. Run: curl -X POST http://localhost:6340/split-layers
# 2. Run: git -C ~/palinode-data diff (see what was classified)
# 3. Add/remove keywords below to correct misclassifications
# 4. Or: add `layer_hint: status` to a file's frontmatter to force-override
#
layer_split:
identity_keywords:
- "architecture"
- "context"
- "people"
- "canon"
- "what this is"
- "key decisions"
- "overview"
- "about"
- "design"
- "stack"
- "key files"
- "follow-up"
- "who"
- "background"
- "principles"
status_keywords:
- "current"
- "status"
- "milestone"
- "active"
- "this week"
- "open"
- "consolidation log"
- "todo"
- "in progress"
- "recent"
- "progress"
- "now"
- "today"
- "next"
- "blocking"
# Fallback: sections with a date like 2026-03-29 → Status
date_pattern: "\\d{4}-\\d{2}-\\d{2}"
# -----------------------------------------------------------------------------
# Temporal Decay
# -----------------------------------------------------------------------------
# Gates the demand-decay importance re-rank band in hybrid search (a memory's
# effective importance decays toward baseline the longer it goes unread).
decay:
enabled: false
# -----------------------------------------------------------------------------
# Services
# -----------------------------------------------------------------------------
services:
# FastAPI server
api:
host: "127.0.0.1" # SECURITY: bind to localhost only. Set to 0.0.0.0 only if you need LAN access.
port: 6340
log_level: "INFO"
# File watcher daemon
watcher:
# Debounce: ignore rapid file changes within this window
debounce_seconds: 1.0
# -----------------------------------------------------------------------------
# Write-time normalisation
# -----------------------------------------------------------------------------
write:
# Rewrite resolvable relative dates ("yesterday", "last Tuesday", "3 days
# ago") to absolute ones in captured session text, anchored on the session
# date. Quoted text, blockquotes and code are never touched, and vague
# phrases ("recently") and intervals ("last week") are left for a human.
# Set false to store the author's wording; `palinode lint` reports the drift
# either way.
normalize_relative_dates: true
# -----------------------------------------------------------------------------
# Git Integration
# -----------------------------------------------------------------------------
git:
# Auto-commit after saves
auto_commit: true
# Auto-push after commits (requires remote configured)
auto_push: false
# Commit message prefix
commit_prefix: "palinode"
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logging:
# JSONL operation log (relative to memory_dir)
operations_log: "logs/operations.jsonl"
# Log to stdout as well
console: true