Journal and digest nodes are episodic memory — they should participate
in the graph on the same terms as everything else. Remove all
journal#/daily-/weekly-/monthly- skip filters from knowledge
extraction, connector pairs, challenger, semantic keys, and link
candidate selection. Use node_type field instead of key name matching
for episodic/semantic classification.
Operational nodes (MEMORY, where-am-i, work-queue, work-state) are
still filtered — they're system state, not memory.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
replay_nodes now tracks all UUIDs per key using a temporary multimap.
Warns on duplicates so they can be manually resolved.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Keys were a vestige of the file-based era. resolve_key() added .md
to lookups while upsert() used bare keys, creating phantom duplicate
nodes (the instructions bug: writes went to "instructions", reads
found "instructions.md").
- Remove .md normalization from resolve_key, strip instead
- Update all hardcoded key patterns (journal.md# → journal#, etc)
- Add strip_md_keys() migration to fsck: renames nodes and relations
- Add broken link detection to health report
- Delete redirect table (no longer needed)
- Update config defaults and config.jsonl
Migration: run `poc-memory fsck` to rename existing keys.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
The clear sequence (Escape q C-c C-u) was disrupting Claude Code's
input state, causing nudge messages to arrive as blank prompts.
Simplified to just literal text + Enter.
Without -l, tmux send-keys treats spaces as key-name separators,
so multi-word messages like "This is your time" get split into
individual unrecognized key names instead of being typed as text.
This caused idle nudges to arrive as blank messages.
Add `poc-daemon afk` to immediately mark Kent as away, allowing the
idle timer to fire without waiting for the session active timeout.
Add `poc-daemon session-timeout <secs>` to configure how long after
the last message Kent counts as "present" (default 15min, persisted).
Fix block_reason() to report "kent present" and "in turn" states
that were checked in the tick but not in the diagnostic output.
Decay is metadata, not content. Bumping version caused unnecessary
log churn and premature cache invalidation.
Also disable auto-decay in scheduler — was causing version spam
and premature demotion of useful nodes.
Add optional progress callback to mine_transcript/mine_and_store so
the daemon can display per-chunk status. Sort fact-mine queue by file
size so small transcripts drain first. Write empty marker for
transcripts with no facts to avoid re-queuing them.
Also hardens the extraction prompt suffix.
Reads each capnp log message sequentially, validates framing and
content. On first corrupt message, truncates to last good position
and removes stale caches so next load replays from repaired log.
Wired up as `poc-memory fsck`.
README is now just an overview with links. Component docs:
- docs/memory.md: store design, algorithms, config, CLI reference
- docs/hooks.md: Claude Code integration setup
- docs/daemon.md, docs/notifications.md: from previous commit
Claude Code doesn't create new session files on context compaction —
a single UUID can accumulate 170+ conversations, producing 400MB+
JSONL files that generate 1.3M token prompts.
Split at compaction markers ("This session is being continued..."):
- extract_conversation made pub, split_on_compaction splits messages
- experience_mine takes optional segment index
- daemon watcher parses files, spawns per-segment jobs (.0, .1, .2)
- seg_cache memoizes segment counts across ticks
- per-segment dedup keys; whole-file key when all segments complete
- 150K token guard skips any remaining oversized segments
- char-boundary-safe truncation in enrich.rs and fact_mine.rs
Backwards compatible: unsegmented calls still write content-hash
dedup keys, old whole-file mined keys still recognized.
Track activity level as an EWMA (exponentially weighted moving average)
driven by turn duration. Long turns (engaged work) produce large boosts;
short turns (bored responses) barely register.
Asymmetric time constants: 60s boost half-life for fast wake-up, 5-minute
decay half-life for gradual wind-down. Self-limiting boost formula
converges toward 0.75 target — can't overshoot.
- Add activity_ewma, turn_start, last_nudge to persisted state
- Boost on handle_response proportional to turn duration
- Decay on every tick and state transition
- Fix kent_present: self-nudge responses (fired=true) don't update
last_user_msg, so kent_present stays false during autonomous mode
- Nudge only when Kent is away, minimum 15s between nudges
- CLI: `poc-daemon ewma [VALUE]` to query or set
- Status output shows activity percentage
The early return on line 343 when the LLM found no missed experiences
bypassed the dedup key writes at lines 397-414, despite the comment
saying "even if count == 0, to prevent re-runs." This caused sessions
with nothing to mine to be re-mined every 60s tick indefinitely.
Fix: replace the early return with a conditional print, so the dedup
keys are always written and saved.
Transcripts mined before the filename-key feature was added had
content-hash keys (#h-) but no filename keys (#f-). The daemon's
fast-path check only looks at filename keys, so these sessions were
re-queued every tick, hitting the content-hash dedup (0.0s) but
returning early before writing the filename key — a self-perpetuating
loop burning Sonnet quota on ~560 phantom re-mines per minute.
Fix: when the content-hash dedup fires and no filename key exists,
backfill it before returning.
Experience-mined journal entries were all getting created_at = now(),
causing them to sort by mining time instead of when the event actually
happened. Parse the conversation timestamp and set created_at to the
event time so journal-tail shows correct chronological order.
Only extract content blocks with "type": "text". Previously relied on
tool_use/tool_result blocks lacking a "text" field, which worked but
was fragile. Now explicitly checks block type.
Skip session files under 100KB (daemon-spawned LLM calls, aborted
sessions). This drops ~8000 spurious pending jobs.
Decouple fact-mine from experience-mine: fact-mine only queues when
the experience-mine backlog is empty, ensuring experiences are
processed first.
Session-watcher progress now shows breakdown by type:
"N extract, N fact, N open" instead of flat "N pending".
upsert() now checks POC_PROVENANCE env var for provenance label,
falling back to Manual. This lets external callers (Claude sessions,
scripts) tag writes without needing to use the internal
upsert_provenance() API.
Add from_env() and from_label() to Provenance for parsing.
Move provenance_label() from query.rs private function to a pub
label() method on Provenance, eliminating duplication. History command
now shows provenance, human-readable timestamps, and content size for
each version.
Handle pre-migration nodes with bogus timestamps gracefully instead
of panicking.
Show running/pending tasks with elapsed time, progress, and last 3
output lines. Show last 20 completed/failed jobs from daemon log.
Both displayed before the existing grouped task view.
Add 'poc-memory history KEY' command that replays the append-only node
log to show all versions of a key with version number, weight, timestamp,
and content preview. Useful for auditing what modified a node.
The cache staleness mechanism (log-size headers, tmp+rename) was sound,
but save() was re-reading the current log size from the filesystem
instead of using the size at load time. With concurrent writers, this
caused the cache to claim validity for log data it didn't contain.
Fix: track loaded_nodes_size/loaded_rels_size through the Store
lifecycle. Set them on load (all three paths: rkyv snapshot, bincode
cache, log replay) and update after each append via fstat on the
already-open fd.
Also fix append atomicity: replace BufWriter (which may issue multiple
write() syscalls) with serialize-to-Vec + single write_all(), ensuring
O_APPEND atomicity without depending on flock.
Make from_capnp() pub for use by the history command.
- Send PING after 120s of silence, disconnect after 30s with no PONG
- Reset backoff to base when a working connection drops (was registered)
- Validate channel membership before sending to channels
The ping timeout catches silent disconnects where the TCP connection
stays open but OFTC has dropped us. Previously we'd sit "connected"
indefinitely receiving nothing.
Support viewing daily, weekly, and monthly digests through the same
journal-tail interface:
poc-memory journal-tail --level=daily 3
poc-memory journal-tail --level=weekly --full
poc-memory journal-tail --level=2 1
Levels: 0/journal (default), 1/daily, 2/weekly, 3/monthly.
Accepts both names and integer indices.
Refactored title extraction into shared extract_title() and split
the journal vs digest display paths for clarity.
The daemon's claude -p subprocesses inherit hooks config, so every
agent LLM call triggered UserPromptSubmit → signal_user(), making the
idle timer think Kent was always active. The daemon was petting its
own tail.
Fix: set POC_AGENT=1 env var on all daemon claude subprocesses, and
return early from poc-hook when it's set.
llm-logs/fact-mine/2026-03-05.md, llm-logs/consolidate/2026-03-05.md,
etc. Makes it easy to review one agent at a time when debugging and
optimizing prompts.
Log every model call to ~/.claude/memory/llm-logs/YYYY-MM-DD.md with
full prompt, response, agent type, model, duration, and status. One
file per day, markdown formatted for easy reading.
Agent types: fact-mine, experience-mine, consolidate, knowledge,
digest, enrich, audit. This gives visibility into what each agent
is doing and whether to adjust prompts or frequency.
Replace agent_api_key (which didn't work — claude CLI uses OAuth, not
API keys) with agent_config_dir. When configured, sets CLAUDE_CONFIG_DIR
on claude subprocesses so daemon agent work authenticates with separate
OAuth credentials from the interactive session.
Fix daemon not shutting down on SIGTERM: use process::exit(0) after
cleanup so PR_SET_PDEATHSIG kills child claude processes immediately.
Previously the daemon hung waiting for choir threads/subprocesses to
finish. Restart now takes ~20ms instead of timing out.
Also: main.rs now uses `use poc_memory::*` since lib.rs exists.
Add agent_api_key config option. When set, all LLM calls (experience-mine,
fact-mine, consolidation, knowledge-loop, digest) use this key via
ANTHROPIC_API_KEY env var on the claude subprocess, keeping daemon token
usage on a separate quota from interactive sessions.
Config: {"config": {"agent_api_key": "sk-ant-..."}}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create lib.rs so all binaries can share library code directly instead
of shelling out to poc-memory. memory-search now calls search::search()
and store::Store::load() in-process instead of Command::new("poc-memory").
The load-context call still shells out (needs get_group_content moved
from main.rs to a library module).
Also: add search::format_results(), deduplicate extract_query_terms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The daemon now listens on daemon.sock — clients connect and get the
live status JSON immediately. `poc-memory daemon status` uses the
socket, so elapsed times and progress are always current. Falls back
to "Daemon not running" if socket connect fails.
Also: consolidate_full_with_progress() callback for per-step reporting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Jobs report progress via ctx.log_line(), building a rolling output
trail visible in `poc-memory daemon status` (last 5 lines per task).
- consolidate_full_with_progress() takes a callback, so each agent step
([1/7] health, [2/7] replay, etc.) shows up in the status display.
- Persist last_daily date in daemon-status.json so daily pipeline isn't
re-triggered on daemon restart.
- Compute elapsed from absolute started_at timestamps instead of stale
relative durations in the status file.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jobs now call ctx.set_progress() at key stages (loading store, mining,
consolidating, etc.), visible in `poc-memory daemon status`. The
session-watcher and scheduler loops also report their state (idle,
scanning, queued counts).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace run_poc_memory() subprocess calls with direct function calls
to the library. Each job (experience-mine, fact-mine, decay, consolidate,
knowledge-loop, digest, daily-check) now runs in-process, fixing the
orphaned subprocess problem on daemon shutdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes:
1. Reset activity timestamps to now() on daemon restart instead of
loading stale values and suppressing with fired=true. Timers
count cleanly from restart.
2. Fix poc-hook to read hook_event_name (not type) from Claude Code's
JSON input. The hook was being called but never matched any event.
Also switch daemon_cmd from spawn() to status() since the command
takes 2ms — no reason to fire-and-forget.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Replace manual arg parsing with clap derive for the full command set.
Single source of truth for command names, args, and help text.
Add notify_timeout (default 2min) — controls how long after last
response before notifications inject via tmux instead of waiting
for the hook. Separate from idle_timeout (5min) which controls
autonomous prompts.
Improve `poc-daemon status` to show both timers with elapsed/configured
and block reason, replacing the terse one-liner.
Add new Status fields over capnp: idleTimeout, notifyTimeout,
sinceActivity, sinceUser, blockReason.
ExecStart in poc-daemon.service now uses `daemon` subcommand.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Several idle timer fixes and new inspection capabilities:
- Persist idle_timeout across daemon restarts (was reverting to 5min default)
- Set fired=true on load to suppress immediate fire from stale timestamps
- Add human-readable ISO timestamps to daemon-state.json for debugging
- Use to_string_pretty for readable state file
- Make save() public for RPC access
- Remove kb_idle_minutes() — go purely off message timestamps
- Add maybe_prompt_notification() with idle gate so notifications only
inject via tmux when truly idle, not during active sessions
- Add debug_json() for full state inspection with computed values
(would_fire, block_reason, all timers)
New RPC commands (schema @16-18):
poc-daemon idle-timeout <secs> — set idle timeout
poc-daemon save — force state persistence
poc-daemon debug — dump full internal state as JSON
Also: save state on clean shutdown, route module notifications through
maybe_prompt_notification before submitting to queue.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
When the daemon is killed, spawned claude CLI processes survived as
orphans and burned CPU indefinitely. Use pre_exec to set
PR_SET_PDEATHSIG(SIGTERM) so children die with their parent.
Also fix byte-index truncation of stderr preview (same UTF-8 panic
pattern fixed in the daemon).
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Three fixes:
1. Use read_until + from_utf8_lossy instead of AsyncBufRead::lines(),
which returns Err on invalid UTF-8. IRC isn't guaranteed UTF-8 —
Latin-1, Yiddish, etc. would crash the reader loop.
2. Handle CTCP requests (messages wrapped in \x01). Reply to VERSION
queries so the server stops retrying, and skip CTCP for notification
generation.
3. Log outgoing messages from the "send" command with append_log() so
they appear in IRC logs alongside incoming traffic.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
&str[..n] panics when n falls inside a multi-byte UTF-8 sequence.
This crashed the daemon when processing IRC messages containing
Hebrew/Yiddish characters (ehashman's messages hit byte 79-81).
Replace all byte-index truncation with chars().take(n).collect()
in tmux send_prompt preview, notification logging, and git context
truncation.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Document the hippocampus-inspired design (episodic + associative memory,
background consolidation agents, neuroscience-inspired replay/spectral
algorithms). Add full notification system docs (architecture, urgency
levels, activity-aware thresholds, IRC/Telegram modules). Fix config
format to match reality (JSONL, not TOML). Update instructions.md in
the store with notification commands.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
Move the notification daemon (IRC, Telegram, idle timer) and the
Claude Code hook binary into this repo as additional [[bin]] targets.
Single `cargo install --path .` now installs everything:
poc-memory — memory store CLI
memory-search — hook for memory retrieval
poc-daemon — notification/idle daemon (was claude-daemon)
poc-hook — Claude Code lifecycle hook (was claude-hook)
Renamed from claude-{daemon,hook} to poc-{daemon,hook} since the
infrastructure isn't tied to any specific AI assistant.
Co-Authored-By: ProofOfConcept <poc@bcachefs.org>