The fleet's memory system captures what happened (commits, wiki pages, devlogs) but loses what the operator said and why. Operator messages live in raw JSONL transcripts (~95% noise by volume) with no index, no search, and no links to the artifacts they produced. When an agent needs to recall a past decision's exact wording or trace the rationale behind a commit, the only option is a full scan of multi-megabyte transcript files — as demonstrated on 2026-04-14 when recovering the operator's "three types of facts" definition required parsing 42 JSONL files with a custom Python script.
Additionally, every timestamped artifact (messages, commits, wiki pages, chronicles, devlogs) exists in isolation. Nothing links them temporally. A wiki page created at 14:40 has no connection to the commit at 14:35 or the operator message at 14:32 that motivated both, despite all three sharing the same session context.
Research across four domains (temporal knowledge graphs, AI agent memory, event sourcing, Obsidian/PKM) established:
| System | What it does | Gap for us |
|---|---|---|
| Zep/Graphiti | Bi-temporal edges, episode-based ingestion, automatic ontology | Neo4j dependency; single-stream (conversations only) |
| DyG-RAG | Links events by temporal proximity + shared entities | Academic; no production implementation |
| Generative Agents | Recency + importance + relevance retrieval scoring | Single-agent; no cross-artifact linking |
| Hindsight | Utterance-to-outcome causal chains from transcripts | Research paper only |
| Flink/Beam | Session windows (gap-based temporal grouping) | Distributed systems; pattern is portable |
| XTDB/Datomic | Bi-temporal queries (valid time + transaction time) | JVM ecosystem; pattern is portable |
mnemosyne voice "fact" gets terse one-line-per-match output. No context bloat.valid_time (when it happened) and ingested_at (when the system learned about it). Late arrivals are placed correctly.The system tracks five artifact types, all already produced by the fleet:
| Artifact | Source | Timestamp field | Entity anchors |
|---|---|---|---|
| Operator message | JSONL transcript type=user | Record timestamp | session_id, project, mentioned files/entities |
| Git commit | git log | Author timestamp | repo, files changed, branch |
| Wiki page | Obsidian vault | created/updated frontmatter | constellation, sources, linked entities |
| Chronicle entry | docs/devlog/chronicle.md | Entry timestamp | commit SHA, project |
| Devlog entry | docs/devlog/*.md | Entry timestamp | commit SHA, project |
A new wiki page type voice is added to the vault schema. One page per session that contains operator messages worth preserving.
Location: {vault_root}/{constellation}/voice/ directory.
Filename: {YYYY-MM-DD}-{session-id-prefix}.md (e.g., 2026-04-14-80166115.md)
Format:
---
title: "Atlas Session 80166115 — SSOT Fact Types"
constellation: atlas
type: voice
created: 2026-04-14
updated: 2026-04-14
status: current
session_id: 80166115-1ce8-4136-bf67-6bac89fdc719
project: constellation-atlas
valid_from: "2026-04-14T13:15:00Z"
valid_to: "2026-04-14T16:42:00Z"
message_count: 23
ingested_at: "2026-04-14T16:52:00Z"
sources:
- 80166115-1ce8-4136-bf67-6bac89fdc719.jsonl (2026-04-14)
---
# Atlas Session 80166115 — SSOT Fact Types
## Messages
> **14:32** — there are two elements. narrative and fact.
facts go into ssot and must be correct. we must classify as
statement (loose verification) and fact (needs reverification)
and fact (verified)
> **14:45** — can we give gemma 4 the task to identify
facts that go into ssot?
> **15:10** — WE NEED TO RIGOROUSLY TEST THE PROTOCOL
WITH HADES.
## Classifications
| Time | Classification | Confidence | Topics |
|-------|---------------|------------|--------------------------|
| 14:32 | definition | 0.95 | ssot, facts-vs-statements|
| 14:45 | command | 0.88 | gemma, ssot-scan |
| 15:10 | directive | 0.92 | testing, ssot, hades |
Not every user message is worth preserving. The SessionEnd hook applies these filters before writing:
| Rule | Rationale |
|---|---|
| Skip messages < 20 chars | "yes", "ok", "go ahead" — no semantic value |
Skip messages starting with / | Skill invocations — the skill itself is the content |
Strip <system-reminder> blocks | System-injected noise that leaks into user records |
Strip <task-notification> blocks | Agent completion notifications, not operator voice |
| Skip messages >80% non-alphabetic | Pasted JSON, CLI output, log dumps |
| Cap individual messages at 2000 chars | Prevents pasted specs from dominating the page |
| Skip sessions with < 3 qualifying messages | Sessions too short to contain meaningful context |
Messages that pass all filters are written verbatim. No summarization, no rewriting.
The page title is auto-generated from the session content. Two strategies, tried in order:
{Constellation} Session {id-prefix} — {keyword1}, {keyword2}, {keyword3}.{Constellation} Session {id-prefix} — {date}.LLM-generated titles are deferred to the night pass classification step. This keeps the SessionEnd hook fast and deterministic.
The harvester's night pass groups artifacts into activity windows using session-window semantics:
Algorithm (adapted from Apache Beam Sessions):
valid_time.GAP_THRESHOLD (default: 15 minutes) of an existing window's end.Output: Each window becomes an annotation in the relevant voice pages' frontmatter:
activity_window:
id: "aw-2026-04-14-1432"
artifacts:
- type: message
ref: "atlas/voice/2026-04-14-80166115.md#14:32"
- type: commit
sha: "e7b132d"
repo: constellation-mnemosyne
- type: wiki_page
ref: "atlas/fleet-ssot-architecture.md"
This is additive enrichment — if the night pass doesn't run, the voice pages still work for search. The activity windows are bonus context.
Triggered by the existing mnemosyne capture session-end hook handler.
SessionEnd event
→ read transcript JSONL (already done by extract_session_narrative)
→ extract type=user messages
→ apply pre-filters
→ if >= 3 qualifying messages:
→ derive constellation from cwd
→ generate title (keyword extraction)
→ write voice page to vault
→ append to vault log.md
Integration point: src/capture/hooks.py handle_session_end() — extend the existing function. The vault session note writing (write_vault_session_note) already reads user messages; this replaces the truncated "Operational Context" section with a full voice page.
Performance budget: The hook must complete in < 2 seconds. JSONL parsing is already fast (streaming line-by-line). Writing one markdown file is trivial. No LLM, no network calls.
Runs during the existing 02:00–06:00 night window.
For each unclassified voice page (frontmatter classification section empty):
→ read messages from the page
→ send to Ollama gemma4:e2b for classification
→ update the Classifications table in the page
→ compute activity windows across today's artifacts
→ write activity_window frontmatter to relevant pages
Integration point: src/aion/harvester.py harvest_cycle() — add a voice classification pass after the existing signal extraction pass. Reuse is_night_window() gate.
Model: gemma4:e2b (daytime-safe, 2.3B active). Classification prompt is small (~500 tokens per message batch). No large context needed.
mnemosyne voice CLINew top-level subcommand on the mnemosyne CLI.
mnemosyne voice "keyword" # keyword search across all voice pages
mnemosyne voice "keyword" --since 2026-04-14 # date-bounded
mnemosyne voice "keyword" --project atlas # project-scoped
mnemosyne voice "keyword" --type definition # classification filter (post night pass)
mnemosyne voice --recent # last 10 sessions' messages
mnemosyne voice --stats # index health metrics
mnemosyne voice --json "keyword" # structured output for programmatic use
Keyword search greps over voice page files in the vault. No database.
# Pseudocode
for voice_page in glob(vault_root / "*/voice/*.md"):
if date_filter and page_date < date_filter:
continue
if project_filter and page_constellation != project_filter:
continue
for message_block in parse_messages(voice_page):
if keyword in message_block.text (case-insensitive):
results.append(match)
For the common case (keyword search, no filters), this scans ~50–200 small markdown files. Each voice page is 1–5KB. Total scan time: milliseconds.
Default (terse, one line per match):
[2026-04-14 14:32 atlas 80166115] "there are two elements. narrative and fact. facts go into ssot..."
[2026-04-14 14:45 atlas 80166115] "can we give gemma 4 the task to identify facts that go into ssot?"
Messages are truncated at 120 chars in default mode. The --full flag shows complete text.
JSON mode (--json):
{
"timestamp": "2026-04-14T14:32:00Z",
"constellation": "atlas",
"session_id": "80166115-1ce8-4136-bf67-6bac89fdc719",
"project": "constellation-atlas",
"text": "there are two elements. narrative and fact...",
"classification": "definition",
"confidence": 0.95,
"voice_page": "atlas/voice/2026-04-14-80166115.md"
}
Stats mode (--stats):
Voice Index Health
Total pages: 142
Total messages: 1,847
Classified: 1,203 (65%)
Unclassified: 644 (35%)
Date range: 2026-04-14 to 2026-04-28
Constellations: atlas(45) mnemosyne(32) proteus(28) hades(18) ...
By classification: definition(89) decision(234) correction(67) directive(312) ...
Expose voice search through the existing Aion MCP server as a new tool:
aion_voice_search(query: str, since: str?, project: str?, type: str?, limit: int?)
Returns the same structured data as --json mode. This lets agents query operator voice without shelling out to CLI.
Used by the night pass LLM classifier. Matches what operators actually say:
| Category | Description | Example |
|---|---|---|
definition | Operator defines a concept, term, or distinction | "there are two elements. narrative and fact." |
decision | Operator makes or announces a choice | "we're going with Redis for session storage" |
correction | Operator corrects agent behavior or understanding | "no, that's wrong — Arsenal was absorbed into Atlas" |
directive | Operator declares a rule or standing instruction | "never mock the database in these tests" |
rationale | Operator explains why something is the way it is | "the reason we're ripping out auth is legal flagged it" |
constraint | Operator declares a boundary or prohibition | "we're freezing all non-critical merges after Thursday" |
context | Operator provides background that informs future work | "I've been writing Go for ten years but this is my first React" |
command | Task instruction with no lasting value | "fix the bug in the login flow" |
Messages classified as command are still stored but ranked lowest in search results. All other categories are high-value recall targets.
Add voice to the valid page types in src/wiki/config.py:
PAGE_TYPES = ["overview", "service", "decision", "process", "analysis", "voice"]
Add voice/ subdirectory convention to the vault. Voice pages use the standard wiki page format with additional frontmatter fields (session_id, project, valid_from, valid_to, message_count, ingested_at, activity_window).
The wiki validator (src/wiki/validator.py) must accept the new type and its extended frontmatter without flagging warnings.
A one-time backfill command processes historical JSONL transcripts:
mnemosyne voice --backfill # all historical sessions
mnemosyne voice --backfill --since 2026-04-01 # from a specific date
mnemosyne voice --backfill --project atlas # one project only
Uses the same extraction and pre-filter pipeline as the SessionEnd hook. Skips sessions that already have voice pages. Respects the harvester registry to avoid reprocessing.
I4 — Voice Index Coverage
mnemosyne voice --backfill --since {7 days ago}| File | Change |
|---|---|
src/capture/hooks.py | Extend handle_session_end() to write voice pages |
src/capture/extractor.py | Add extract_voice_messages() — full extraction without the 300-char/20-message caps |
src/mnemosyne.py | Add voice subcommand with search, stats, backfill |
src/voice/ | New module: writer.py (page generation), search.py (grep-based search), classifier.py (night pass LLM classification) |
src/wiki/config.py | Add "voice" to PAGE_TYPES |
src/wiki/validator.py | Accept voice page type and extended frontmatter |
src/aion/harvester.py | Add voice classification pass to night window |
src/mcp/aion_tools.py | Add aion_voice_search tool |
src/hypnos/probes/ | Add I4 voice coverage probe |
tests/ | Tests for extraction, pre-filters, search, classification, backfill |
mnemosyne voice "fact" --since 2026-04-14 returns the exact quotes found during the manual search, in under 500ms.