personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move day content under journal/chronicle/

Relative paths stay chronicle-free; the prefix is applied at the
rel↔abs boundary via resolve_journal_path(), CHRONICLE_DIR, and the
updated day_path()/day_dirs(). Indexer, formatters, hooks, and merge
thread through the new boundary. apps/sol/maint/002_migrate_chronicle.py
relocates existing day dirs on next startup and forces a clean
reindex; merge.py is the only production code that reads both layouts
(for external archives). All ~67 fixture day dirs moved via git mv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+799 -372
+6 -6
apps/entities/call.py
··· 13 13 import typer 14 14 15 15 from think.entities.core import entity_slug, is_valid_entity_type 16 + from think.entities.journal import ( 17 + clear_journal_entity_cache, 18 + get_or_create_journal_entity, 19 + load_journal_entity, 20 + save_journal_entity, 21 + ) 16 22 from think.entities.loading import clear_entity_loading_cache, load_entities 17 23 from think.entities.matching import resolve_entity, validate_aka_uniqueness 18 24 from think.entities.observations import ( ··· 25 31 entity_memory_path, 26 32 load_facet_relationship, 27 33 save_facet_relationship, 28 - ) 29 - from think.entities.journal import ( 30 - clear_journal_entity_cache, 31 - get_or_create_journal_entity, 32 - load_journal_entity, 33 - save_journal_entity, 34 34 ) 35 35 from think.entities.saving import ( 36 36 save_detected_entity,
+115
apps/sol/maint/002_migrate_chronicle.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Migrate root day directories into chronicle/.""" 5 + 6 + from __future__ import annotations 7 + 8 + import argparse 9 + import shutil 10 + from dataclasses import dataclass 11 + from pathlib import Path 12 + 13 + from think.utils import CHRONICLE_DIR, DATE_RE, get_journal, setup_cli 14 + 15 + 16 + @dataclass 17 + class MigrationSummary: 18 + """Mutable counters for migration operations.""" 19 + 20 + moved: int = 0 21 + skipped: int = 0 22 + sqlite_deleted: int = 0 23 + errors: int = 0 24 + 25 + 26 + def _root_day_dirs(journal_path: Path) -> list[Path]: 27 + return [ 28 + path 29 + for path in sorted(journal_path.iterdir()) 30 + if path.is_dir() and DATE_RE.fullmatch(path.name) 31 + ] 32 + 33 + 34 + def _sqlite_paths(journal_path: Path) -> list[Path]: 35 + db_path = journal_path / "indexer" / "journal.sqlite" 36 + return [ 37 + db_path, 38 + db_path.with_name(f"{db_path.name}-wal"), 39 + db_path.with_name(f"{db_path.name}-shm"), 40 + ] 41 + 42 + 43 + def _validate_end_state(journal_path: Path) -> None: 44 + if _root_day_dirs(journal_path): 45 + raise RuntimeError("root day directories remain after chronicle migration") 46 + if not (journal_path / CHRONICLE_DIR).is_dir(): 47 + raise RuntimeError("chronicle/ missing after chronicle migration") 48 + remaining_sqlite = [path for path in _sqlite_paths(journal_path) if path.exists()] 49 + if remaining_sqlite: 50 + joined = ", ".join(str(path) for path in remaining_sqlite) 51 + raise RuntimeError(f"sqlite files remain after chronicle migration: {joined}") 52 + 53 + 54 + def migrate(journal_path: Path, dry_run: bool = False) -> MigrationSummary: 55 + """Migrate root day directories into chronicle/.""" 56 + summary = MigrationSummary() 57 + day_dirs = _root_day_dirs(journal_path) 58 + if not day_dirs: 59 + print("Nothing to migrate.") 60 + return summary 61 + 62 + chronicle_dir = journal_path / CHRONICLE_DIR 63 + if dry_run: 64 + print("[DRY-RUN] No files will be modified.") 65 + else: 66 + chronicle_dir.mkdir(parents=True, exist_ok=True) 67 + 68 + print(f"Migrating chronicle dirs in: {journal_path}") 69 + for source_day in day_dirs: 70 + target_day = chronicle_dir / source_day.name 71 + if target_day.exists(): 72 + print(f" Skip (already exists): {source_day.name}") 73 + summary.skipped += 1 74 + continue 75 + 76 + print(f" Move: {source_day.name} -> {CHRONICLE_DIR}/{source_day.name}") 77 + if not dry_run: 78 + shutil.move(str(source_day), str(target_day)) 79 + summary.moved += 1 80 + 81 + if summary.moved and not dry_run: 82 + for sqlite_path in _sqlite_paths(journal_path): 83 + if not sqlite_path.exists(): 84 + continue 85 + print(f" Delete: {sqlite_path.relative_to(journal_path)}") 86 + sqlite_path.unlink() 87 + summary.sqlite_deleted += 1 88 + _validate_end_state(journal_path) 89 + 90 + return summary 91 + 92 + 93 + def main() -> None: 94 + """CLI entrypoint.""" 95 + parser = argparse.ArgumentParser( 96 + description="Migrate root day directories into chronicle/" 97 + ) 98 + parser.add_argument( 99 + "--dry-run", action="store_true", help="Preview changes without writing" 100 + ) 101 + args = setup_cli(parser) 102 + 103 + summary = migrate(Path(get_journal()), dry_run=args.dry_run) 104 + if summary.moved == 0 and summary.skipped == 0 and summary.sqlite_deleted == 0: 105 + return 106 + 107 + print("Migration complete") 108 + print(f" moved: {summary.moved}") 109 + print(f" skipped: {summary.skipped}") 110 + print(f" sqlite_deleted:{summary.sqlite_deleted}") 111 + print(f" errors: {summary.errors}") 112 + 113 + 114 + if __name__ == "__main__": 115 + main()
+7 -2
apps/sol/routes.py
··· 19 19 from think.facets import get_facets 20 20 from think.models import calc_agent_cost 21 21 from think.talent import get_output_path, get_talent_configs 22 - from think.utils import day_path, updated_days 22 + from think.utils import resolve_journal_path, updated_days 23 23 24 24 sol_bp = Blueprint( 25 25 "app:sol", ··· 510 510 if filename.startswith("facets/"): 511 511 file_path = (journal_root / filename).resolve() 512 512 else: 513 - file_path = (day_path(day, create=False) / filename).resolve() 513 + try: 514 + file_path = resolve_journal_path( 515 + journal_root, f"{day}/{filename}" 516 + ).resolve() 517 + except ValueError: 518 + return jsonify(error="Invalid path"), 403 514 519 515 520 # Security: ensure path is within the journal directory 516 521 try:
-2
apps/speakers/voiceprint_io.py
··· 11 11 12 12 import fcntl 13 13 import logging 14 - import os 15 - import re 16 14 from pathlib import Path 17 15 18 16 import numpy as np
+1 -1
apps/support/portal.py
··· 403 403 raise RuntimeError( 404 404 "could not register — all handle variants were taken after 3 attempts" 405 405 ) 406 - import re 407 406 import random 407 + import re 408 408 import string 409 409 410 410 base = re.sub(r"-[a-z0-9]{4}$", "", self._handle)
+1 -2
convey/__init__.py
··· 17 17 18 18 from apps import AppRegistry 19 19 20 - from . import state 21 - from . import system 20 + from . import state, system 22 21 from .apps import register_app_context 23 22 from .bridge import emit, register_websocket 24 23 from .config import bp as config_bp
+9 -3
observe/describe.py
··· 36 36 from observe.utils import get_segment_key 37 37 from think.callosum import callosum_send 38 38 from think.prompts import load_prompt 39 - from think.utils import day_from_path, get_config, get_journal, setup_cli 39 + from think.utils import ( 40 + day_from_path, 41 + get_config, 42 + get_journal, 43 + journal_relative_path, 44 + setup_cli, 45 + ) 40 46 41 47 logger = logging.getLogger(__name__) 42 48 ··· 946 952 journal_path = Path(get_journal()) 947 953 948 954 try: 949 - rel_input = video_path.relative_to(journal_path) 950 - rel_output = output_path.relative_to(journal_path) 955 + rel_input = journal_relative_path(journal_path, video_path) 956 + rel_output = journal_relative_path(journal_path, output_path) 951 957 except ValueError: 952 958 rel_input = video_path 953 959 rel_output = output_path
+49 -29
observe/sense.py
··· 24 24 from observe.utils import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS 25 25 from think.callosum import CallosumConnection 26 26 from think.runner import ManagedProcess as RunnerManagedProcess 27 - from think.utils import day_path, get_journal, get_rev, iter_segments, now_ms, setup_cli 27 + from think.utils import ( 28 + CHRONICLE_DIR, 29 + DATE_RE, 30 + day_path, 31 + get_journal, 32 + get_rev, 33 + iter_segments, 34 + journal_relative_path, 35 + now_ms, 36 + resolve_journal_path, 37 + setup_cli, 38 + ) 28 39 29 40 logger = logging.getLogger(__name__) 30 41 ··· 173 184 self.handlers[pattern] = (handler_name, command) 174 185 logger.info(f"Registered handler '{handler_name}' for pattern '{pattern}'") 175 186 187 + def _segment_relative_path(self, file_path: Path) -> Optional[Path]: 188 + """Return day/stream/segment/file relative path for journal media files.""" 189 + roots = [self.journal_dir / CHRONICLE_DIR, self.journal_dir] 190 + for root in roots: 191 + try: 192 + rel_path = file_path.relative_to(root) 193 + except ValueError: 194 + continue 195 + if len(rel_path.parts) == 4 and DATE_RE.fullmatch(rel_path.parts[0]): 196 + return rel_path 197 + return None 198 + 176 199 def _match_pattern(self, file_path: Path) -> Optional[tuple[str, List[str]]]: 177 200 """Check if file matches any registered pattern.""" 178 201 # Ignore hidden files (temp recordings with dot prefix) 179 202 if file_path.name.startswith("."): 180 203 return None 181 204 182 - # Files should be in segment directories: journal_dir/YYYYMMDD/stream/HHMMSS_LEN/file.ext 183 - # Expected structure: 4 parts from journal_dir 184 - try: 185 - rel_path = file_path.relative_to(self.journal_dir) 186 - if len(rel_path.parts) != 4: 187 - return None 188 - except ValueError: 189 - # File not under journal directory 205 + # Files should be in segment directories: 206 + # journal_dir/chronicle/YYYYMMDD/stream/HHMMSS_LEN/file.ext 207 + # Expected structure after stripping journal_dir[/chronicle]: 4 parts 208 + if self._segment_relative_path(file_path) is None: 190 209 return None 191 210 192 211 for pattern, handler_info in self.handlers.items(): ··· 208 227 ): 209 228 """Spawn a handler process for the file. 210 229 211 - Files are expected to be in segment directories: YYYYMMDD/stream/HHMMSS_LEN/file.ext 230 + Files are expected to be in segment directories: 231 + YYYYMMDD/stream/HHMMSS_LEN/file.ext relative to journal_dir[/chronicle] 212 232 213 233 Args: 214 234 file_path: Path to the file to process (in segment directory) ··· 223 243 cpu_fallback: If True, this is a retry after GPU failure (adds --cpu, 224 244 skips tracking/events since already done on first attempt) 225 245 """ 226 - # Extract day and segment from path: journal_dir/YYYYMMDD/stream/HHMMSS_LEN/file.ext 227 - try: 228 - rel_path = file_path.relative_to(self.journal_dir) 229 - if len(rel_path.parts) >= 4: 230 - if day is None: 231 - day = rel_path.parts[0] 232 - if segment is None: 233 - segment = rel_path.parts[2] 234 - except ValueError: 235 - pass 246 + # Extract day and segment from path relative to journal_dir[/chronicle]. 247 + rel_path = self._segment_relative_path(file_path) 248 + if rel_path is not None: 249 + if day is None: 250 + day = rel_path.parts[0] 251 + if segment is None: 252 + segment = rel_path.parts[2] 236 253 237 254 # Skip tracking/queueing for CPU fallback (already done on first attempt) 238 255 if not cpu_fallback: ··· 617 634 meta = {} 618 635 meta["stream"] = stream 619 636 620 - # Build full paths for all files in this segment 621 - # Files are in segment directories: YYYYMMDD/stream/HHMMSS_LEN/filename 622 - dp = day_path(day, create=False) 623 - segment_dir = dp / stream / segment if stream else dp / segment 637 + # Build full paths for all files in this segment. 638 + rel_segment = f"{day}/{stream}/{segment}" if stream else f"{day}/{segment}" 639 + segment_dir = resolve_journal_path(self.journal_dir, rel_segment) 624 640 file_paths = [segment_dir / filename for filename in files] 625 641 626 642 # Pre-register segment tracking with complete file list ··· 663 679 status = {} 664 680 665 681 # Get journal path for relative paths 666 - journal_path = get_journal() 682 + journal_path = Path(get_journal()) 667 683 now = time.time() 668 684 669 685 # Build status for each serialized handler queue ··· 674 690 if handler_queue.current_process is not None: 675 691 handler_proc = handler_queue.current_process 676 692 try: 677 - rel_file = str(handler_proc.file_path.relative_to(journal_path)) 693 + rel_file = journal_relative_path( 694 + journal_path, handler_proc.file_path 695 + ) 678 696 except ValueError: 679 697 rel_file = str(handler_proc.file_path) 680 698 ··· 691 709 queued_list = [] 692 710 for item in handler_queue.queue: 693 711 try: 694 - rel_file = str(item.file_path.relative_to(journal_path)) 712 + rel_file = journal_relative_path( 713 + journal_path, item.file_path 714 + ) 695 715 except ValueError: 696 716 rel_file = str(item.file_path) 697 717 ··· 1094 1114 if deleted: 1095 1115 logger.info(f"Would delete {len(deleted)} output file(s):") 1096 1116 for path in deleted: 1097 - logger.info(f" {path.relative_to(journal)}") 1117 + logger.info(f" {journal_relative_path(Path(journal), path)}") 1098 1118 else: 1099 1119 logger.info("No files to delete") 1100 1120 return ··· 1116 1136 ) 1117 1137 logger.info(f"Would process {len(to_process)} file(s) ({breakdown}):") 1118 1138 for file_path, handler_name, command in to_process: 1119 - logger.info(f" {file_path.relative_to(journal)}") 1139 + logger.info(f" {journal_relative_path(Path(journal), file_path)}") 1120 1140 else: 1121 1141 logger.info("No unprocessed files found") 1122 1142 return
+9 -4
observe/transcribe/main.py
··· 77 77 get_config, 78 78 get_journal, 79 79 iter_segments, 80 + journal_relative_path, 81 + resolve_journal_path, 80 82 setup_cli, 81 83 ) 82 84 ··· 187 189 day = day_from_path(audio_path) 188 190 189 191 try: 190 - rel_input = audio_path.relative_to(journal_path) 192 + rel_input = journal_relative_path(journal_path, audio_path) 191 193 except ValueError: 192 194 rel_input = audio_path 193 195 ··· 616 618 event["outcome"] = "transcribed" 617 619 event["duration_ms"] = int((time.time() - start_time) * 1000) 618 620 try: 619 - rel_output = jsonl_path.relative_to(journal_path) 621 + rel_output = journal_relative_path(journal_path, jsonl_path) 620 622 except ValueError: 621 623 rel_output = jsonl_path 622 - event["output"] = str(rel_output) 624 + event["output"] = rel_output 623 625 624 626 callosum_send("observe", "transcribed", **event) 625 627 ··· 852 854 853 855 audio_path = Path(args.audio_path) 854 856 if not audio_path.exists(): 855 - journal_relative = Path(get_journal()) / args.audio_path 857 + if audio_path.is_absolute(): 858 + journal_relative = Path(get_journal()) / audio_path.as_posix().lstrip("/") 859 + else: 860 + journal_relative = resolve_journal_path(get_journal(), args.audio_path) 856 861 if journal_relative.exists(): 857 862 audio_path = journal_relative 858 863 else:
+14 -2
observe/transfer.py
··· 29 29 import requests 30 30 31 31 from think.callosum import callosum_send 32 - from think.utils import day_path, get_journal, iter_segments, now_ms, setup_cli 32 + from think.utils import ( 33 + CHRONICLE_DIR, 34 + day_path, 35 + get_journal, 36 + iter_segments, 37 + now_ms, 38 + setup_cli, 39 + ) 33 40 34 41 from .utils import compute_file_sha256, find_available_segment 35 42 ··· 375 382 def _parse_day_spec(day_spec: str | None, journal_root: Path) -> list[str]: 376 383 """Parse a single day, day range, or default to all journal days.""" 377 384 if day_spec is None: 385 + day_root = ( 386 + journal_root / CHRONICLE_DIR 387 + if (journal_root / CHRONICLE_DIR).is_dir() 388 + else journal_root 389 + ) 378 390 return sorted( 379 391 [ 380 392 day_dir.name 381 - for day_dir in journal_root.iterdir() 393 + for day_dir in day_root.iterdir() 382 394 if day_dir.is_dir() and re.match(r"^\d{8}$", day_dir.name) 383 395 ] 384 396 )
+1 -2
pyproject.toml
··· 134 134 override-dependencies = ["webrtcvad>=99; python_version < '0'"] 135 135 136 136 [tool.pytest.ini_options] 137 - addopts = "--import-mode=importlib" 137 + addopts = "--import-mode=importlib --basetemp=/var/tmp/pytest-solstone" 138 138 testpaths = ["tests", "apps"] 139 139 python_files = ["test_*.py"] 140 140 python_classes = ["Test*"] ··· 144 144 ] 145 145 timeout = 5 146 146 tmp_path_retention_policy = "none" 147 - basetemp = "/var/tmp/pytest-solstone" 148 147 filterwarnings = [ 149 148 # Third-party deprecation warnings (Python 3.14+) 150 149 "ignore:codecs.open.* is deprecated:DeprecationWarning",
tests/fixtures/journal/20240101/agents/flow.md tests/fixtures/journal/chronicle/20240101/agents/flow.md
tests/fixtures/journal/20240101/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md
tests/fixtures/journal/20240101/agents/meetings.md tests/fixtures/journal/chronicle/20240101/agents/meetings.md
tests/fixtures/journal/20240101/default/123456_300/agents/audio.md tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md
tests/fixtures/journal/20240101/default/123456_300/agents/screen.md tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md
tests/fixtures/journal/20240101/default/123456_300/audio.json tests/fixtures/journal/chronicle/20240101/default/123456_300/audio.json
tests/fixtures/journal/20240101/default/123456_300/audio.jsonl tests/fixtures/journal/chronicle/20240101/default/123456_300/audio.jsonl
tests/fixtures/journal/20240101/default/123456_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20240101/default/123456_300/monitor_1_diff.json
tests/fixtures/journal/20240101/default/123456_300/monitor_1_diff.png tests/fixtures/journal/chronicle/20240101/default/123456_300/monitor_1_diff.png
tests/fixtures/journal/20240101/default/123456_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20240101/default/123456_300/monitor_1_diff_box.json
tests/fixtures/journal/20240101/default/123456_300/screen.jsonl tests/fixtures/journal/chronicle/20240101/default/123456_300/screen.jsonl
tests/fixtures/journal/20240101/default/123456_300/stream.json tests/fixtures/journal/chronicle/20240101/default/123456_300/stream.json
tests/fixtures/journal/20240101/import.apple/140000_300/audio.jsonl tests/fixtures/journal/chronicle/20240101/import.apple/140000_300/audio.jsonl
tests/fixtures/journal/20240101/import.apple/140000_300/stream.json tests/fixtures/journal/chronicle/20240101/import.apple/140000_300/stream.json
tests/fixtures/journal/20240101/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20240101/indexer/transcripts.sqlite
tests/fixtures/journal/20240102/agents/flow.md tests/fixtures/journal/chronicle/20240102/agents/flow.md
tests/fixtures/journal/20240102/default/234567_300/agents/audio.md tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md
tests/fixtures/journal/20240102/default/234567_300/agents/screen.md tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md
tests/fixtures/journal/20240102/default/234567_300/audio.json tests/fixtures/journal/chronicle/20240102/default/234567_300/audio.json
tests/fixtures/journal/20240102/default/234567_300/audio.jsonl tests/fixtures/journal/chronicle/20240102/default/234567_300/audio.jsonl
tests/fixtures/journal/20240102/default/234567_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20240102/default/234567_300/monitor_1_diff.json
tests/fixtures/journal/20240102/default/234567_300/screen.jsonl tests/fixtures/journal/chronicle/20240102/default/234567_300/screen.jsonl
tests/fixtures/journal/20240102/default/234567_300/stream.json tests/fixtures/journal/chronicle/20240102/default/234567_300/stream.json
tests/fixtures/journal/20240102/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20240102/indexer/transcripts.sqlite
tests/fixtures/journal/20250101/health/stream.updated tests/fixtures/journal/chronicle/20250101/health/stream.updated
tests/fixtures/journal/20250124/TODO.md tests/fixtures/journal/chronicle/20250124/TODO.md
tests/fixtures/journal/20260101/import.chatgpt/100000_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.chatgpt/100000_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.chatgpt/100000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.chatgpt/100000_300/stream.json
tests/fixtures/journal/20260101/import.chatgpt/100500_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.chatgpt/100500_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.chatgpt/100500_300/stream.json tests/fixtures/journal/chronicle/20260101/import.chatgpt/100500_300/stream.json
tests/fixtures/journal/20260101/import.chatgpt/101000_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.chatgpt/101000_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.chatgpt/101000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.chatgpt/101000_300/stream.json
tests/fixtures/journal/20260101/import.claude/140000_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.claude/140000_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.claude/140000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.claude/140000_300/stream.json
tests/fixtures/journal/20260101/import.claude/140500_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.claude/140500_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.claude/140500_300/stream.json tests/fixtures/journal/chronicle/20260101/import.claude/140500_300/stream.json
tests/fixtures/journal/20260101/import.claude/141000_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.claude/141000_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.claude/141000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.claude/141000_300/stream.json
tests/fixtures/journal/20260101/import.gemini/110000_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.gemini/110000_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.gemini/110000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.gemini/110000_300/stream.json
tests/fixtures/journal/20260101/import.gemini/110500_300/conversation_transcript.jsonl tests/fixtures/journal/chronicle/20260101/import.gemini/110500_300/conversation_transcript.jsonl
tests/fixtures/journal/20260101/import.gemini/110500_300/stream.json tests/fixtures/journal/chronicle/20260101/import.gemini/110500_300/stream.json
tests/fixtures/journal/20260101/import.ics/090000_300/event_transcript.md tests/fixtures/journal/chronicle/20260101/import.ics/090000_300/event_transcript.md
tests/fixtures/journal/20260101/import.ics/090000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.ics/090000_300/stream.json
tests/fixtures/journal/20260101/import.ics/093000_300/event_transcript.md tests/fixtures/journal/chronicle/20260101/import.ics/093000_300/event_transcript.md
tests/fixtures/journal/20260101/import.ics/093000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.ics/093000_300/stream.json
tests/fixtures/journal/20260101/import.kindle/200000_300/highlights_transcript.md tests/fixtures/journal/chronicle/20260101/import.kindle/200000_300/highlights_transcript.md
tests/fixtures/journal/20260101/import.kindle/200000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.kindle/200000_300/stream.json
tests/fixtures/journal/20260101/import.kindle/200500_300/highlights_transcript.md tests/fixtures/journal/chronicle/20260101/import.kindle/200500_300/highlights_transcript.md
tests/fixtures/journal/20260101/import.kindle/200500_300/stream.json tests/fixtures/journal/chronicle/20260101/import.kindle/200500_300/stream.json
tests/fixtures/journal/20260101/import.obsidian/160000_300/note_transcript.md tests/fixtures/journal/chronicle/20260101/import.obsidian/160000_300/note_transcript.md
tests/fixtures/journal/20260101/import.obsidian/160000_300/stream.json tests/fixtures/journal/chronicle/20260101/import.obsidian/160000_300/stream.json
tests/fixtures/journal/20260101/import.obsidian/160500_300/note_transcript.md tests/fixtures/journal/chronicle/20260101/import.obsidian/160500_300/note_transcript.md
tests/fixtures/journal/20260101/import.obsidian/160500_300/stream.json tests/fixtures/journal/chronicle/20260101/import.obsidian/160500_300/stream.json
tests/fixtures/journal/20260304/agents/flow.md tests/fixtures/journal/chronicle/20260304/agents/flow.md
tests/fixtures/journal/20260304/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md
tests/fixtures/journal/20260304/agents/meetings.md tests/fixtures/journal/chronicle/20260304/agents/meetings.md
tests/fixtures/journal/20260304/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md
tests/fixtures/journal/20260304/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md
tests/fixtures/journal/20260304/default/090000_300/agents/speaker_labels.json tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json
tests/fixtures/journal/20260304/default/090000_300/agents/speakers.json tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json
tests/fixtures/journal/20260304/default/090000_300/audio.json tests/fixtures/journal/chronicle/20260304/default/090000_300/audio.json
tests/fixtures/journal/20260304/default/090000_300/audio.jsonl tests/fixtures/journal/chronicle/20260304/default/090000_300/audio.jsonl
tests/fixtures/journal/20260304/default/090000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260304/default/090000_300/monitor_1_diff.json
tests/fixtures/journal/20260304/default/090000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260304/default/090000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260304/default/090000_300/screen.jsonl tests/fixtures/journal/chronicle/20260304/default/090000_300/screen.jsonl
tests/fixtures/journal/20260304/default/090000_300/stream.json tests/fixtures/journal/chronicle/20260304/default/090000_300/stream.json
tests/fixtures/journal/20260304/default/140000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md
tests/fixtures/journal/20260304/default/140000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md
tests/fixtures/journal/20260304/default/140000_300/audio.json tests/fixtures/journal/chronicle/20260304/default/140000_300/audio.json
tests/fixtures/journal/20260304/default/140000_300/audio.jsonl tests/fixtures/journal/chronicle/20260304/default/140000_300/audio.jsonl
tests/fixtures/journal/20260304/default/140000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260304/default/140000_300/monitor_1_diff.json
tests/fixtures/journal/20260304/default/140000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260304/default/140000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260304/default/140000_300/screen.jsonl tests/fixtures/journal/chronicle/20260304/default/140000_300/screen.jsonl
tests/fixtures/journal/20260304/default/140000_300/stream.json tests/fixtures/journal/chronicle/20260304/default/140000_300/stream.json
tests/fixtures/journal/20260304/default/180000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md
tests/fixtures/journal/20260304/default/180000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md
tests/fixtures/journal/20260304/default/180000_300/audio.json tests/fixtures/journal/chronicle/20260304/default/180000_300/audio.json
tests/fixtures/journal/20260304/default/180000_300/audio.jsonl tests/fixtures/journal/chronicle/20260304/default/180000_300/audio.jsonl
tests/fixtures/journal/20260304/default/180000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260304/default/180000_300/monitor_1_diff.json
tests/fixtures/journal/20260304/default/180000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260304/default/180000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260304/default/180000_300/screen.jsonl tests/fixtures/journal/chronicle/20260304/default/180000_300/screen.jsonl
tests/fixtures/journal/20260304/default/180000_300/stream.json tests/fixtures/journal/chronicle/20260304/default/180000_300/stream.json
tests/fixtures/journal/20260304/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260304/indexer/transcripts.sqlite
tests/fixtures/journal/20260305/agents/flow.md tests/fixtures/journal/chronicle/20260305/agents/flow.md
tests/fixtures/journal/20260305/agents/meetings.md tests/fixtures/journal/chronicle/20260305/agents/meetings.md
tests/fixtures/journal/20260305/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md
tests/fixtures/journal/20260305/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md
tests/fixtures/journal/20260305/default/090000_300/audio.json tests/fixtures/journal/chronicle/20260305/default/090000_300/audio.json
tests/fixtures/journal/20260305/default/090000_300/audio.jsonl tests/fixtures/journal/chronicle/20260305/default/090000_300/audio.jsonl
tests/fixtures/journal/20260305/default/090000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260305/default/090000_300/monitor_1_diff.json
tests/fixtures/journal/20260305/default/090000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260305/default/090000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260305/default/090000_300/screen.jsonl tests/fixtures/journal/chronicle/20260305/default/090000_300/screen.jsonl
tests/fixtures/journal/20260305/default/090000_300/stream.json tests/fixtures/journal/chronicle/20260305/default/090000_300/stream.json
tests/fixtures/journal/20260305/default/133000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md
tests/fixtures/journal/20260305/default/133000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md
tests/fixtures/journal/20260305/default/133000_300/audio.json tests/fixtures/journal/chronicle/20260305/default/133000_300/audio.json
tests/fixtures/journal/20260305/default/133000_300/audio.jsonl tests/fixtures/journal/chronicle/20260305/default/133000_300/audio.jsonl
tests/fixtures/journal/20260305/default/133000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260305/default/133000_300/monitor_1_diff.json
tests/fixtures/journal/20260305/default/133000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260305/default/133000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260305/default/133000_300/screen.jsonl tests/fixtures/journal/chronicle/20260305/default/133000_300/screen.jsonl
tests/fixtures/journal/20260305/default/133000_300/stream.json tests/fixtures/journal/chronicle/20260305/default/133000_300/stream.json
tests/fixtures/journal/20260305/default/220000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md
tests/fixtures/journal/20260305/default/220000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md
tests/fixtures/journal/20260305/default/220000_300/audio.json tests/fixtures/journal/chronicle/20260305/default/220000_300/audio.json
tests/fixtures/journal/20260305/default/220000_300/audio.jsonl tests/fixtures/journal/chronicle/20260305/default/220000_300/audio.jsonl
tests/fixtures/journal/20260305/default/220000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260305/default/220000_300/monitor_1_diff.json
tests/fixtures/journal/20260305/default/220000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260305/default/220000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260305/default/220000_300/screen.jsonl tests/fixtures/journal/chronicle/20260305/default/220000_300/screen.jsonl
tests/fixtures/journal/20260305/default/220000_300/stream.json tests/fixtures/journal/chronicle/20260305/default/220000_300/stream.json
tests/fixtures/journal/20260305/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260305/indexer/transcripts.sqlite
tests/fixtures/journal/20260306/agents/flow.md tests/fixtures/journal/chronicle/20260306/agents/flow.md
tests/fixtures/journal/20260306/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md
tests/fixtures/journal/20260306/default/093000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md
tests/fixtures/journal/20260306/default/093000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md
tests/fixtures/journal/20260306/default/093000_300/audio.json tests/fixtures/journal/chronicle/20260306/default/093000_300/audio.json
tests/fixtures/journal/20260306/default/093000_300/audio.jsonl tests/fixtures/journal/chronicle/20260306/default/093000_300/audio.jsonl
tests/fixtures/journal/20260306/default/093000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260306/default/093000_300/monitor_1_diff.json
tests/fixtures/journal/20260306/default/093000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260306/default/093000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260306/default/093000_300/screen.jsonl tests/fixtures/journal/chronicle/20260306/default/093000_300/screen.jsonl
tests/fixtures/journal/20260306/default/093000_300/stream.json tests/fixtures/journal/chronicle/20260306/default/093000_300/stream.json
tests/fixtures/journal/20260306/default/110000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md
tests/fixtures/journal/20260306/default/110000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md
tests/fixtures/journal/20260306/default/110000_300/audio.json tests/fixtures/journal/chronicle/20260306/default/110000_300/audio.json
tests/fixtures/journal/20260306/default/110000_300/audio.jsonl tests/fixtures/journal/chronicle/20260306/default/110000_300/audio.jsonl
tests/fixtures/journal/20260306/default/110000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260306/default/110000_300/monitor_1_diff.json
tests/fixtures/journal/20260306/default/110000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260306/default/110000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260306/default/110000_300/screen.jsonl tests/fixtures/journal/chronicle/20260306/default/110000_300/screen.jsonl
tests/fixtures/journal/20260306/default/110000_300/stream.json tests/fixtures/journal/chronicle/20260306/default/110000_300/stream.json
tests/fixtures/journal/20260306/default/143000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md
tests/fixtures/journal/20260306/default/143000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md
tests/fixtures/journal/20260306/default/143000_300/audio.json tests/fixtures/journal/chronicle/20260306/default/143000_300/audio.json
tests/fixtures/journal/20260306/default/143000_300/audio.jsonl tests/fixtures/journal/chronicle/20260306/default/143000_300/audio.jsonl
tests/fixtures/journal/20260306/default/143000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260306/default/143000_300/monitor_1_diff.json
tests/fixtures/journal/20260306/default/143000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260306/default/143000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260306/default/143000_300/screen.jsonl tests/fixtures/journal/chronicle/20260306/default/143000_300/screen.jsonl
tests/fixtures/journal/20260306/default/143000_300/stream.json tests/fixtures/journal/chronicle/20260306/default/143000_300/stream.json
tests/fixtures/journal/20260306/default/170000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md
tests/fixtures/journal/20260306/default/170000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md
tests/fixtures/journal/20260306/default/170000_300/audio.json tests/fixtures/journal/chronicle/20260306/default/170000_300/audio.json
tests/fixtures/journal/20260306/default/170000_300/audio.jsonl tests/fixtures/journal/chronicle/20260306/default/170000_300/audio.jsonl
tests/fixtures/journal/20260306/default/170000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260306/default/170000_300/monitor_1_diff.json
tests/fixtures/journal/20260306/default/170000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260306/default/170000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260306/default/170000_300/screen.jsonl tests/fixtures/journal/chronicle/20260306/default/170000_300/screen.jsonl
tests/fixtures/journal/20260306/default/170000_300/stream.json tests/fixtures/journal/chronicle/20260306/default/170000_300/stream.json
tests/fixtures/journal/20260306/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260306/indexer/transcripts.sqlite
tests/fixtures/journal/20260307/agents/flow.md tests/fixtures/journal/chronicle/20260307/agents/flow.md
tests/fixtures/journal/20260307/agents/meetings.md tests/fixtures/journal/chronicle/20260307/agents/meetings.md
tests/fixtures/journal/20260307/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md
tests/fixtures/journal/20260307/default/100000_300/audio.json tests/fixtures/journal/chronicle/20260307/default/100000_300/audio.json
tests/fixtures/journal/20260307/default/100000_300/audio.jsonl tests/fixtures/journal/chronicle/20260307/default/100000_300/audio.jsonl
tests/fixtures/journal/20260307/default/100000_300/stream.json tests/fixtures/journal/chronicle/20260307/default/100000_300/stream.json
tests/fixtures/journal/20260307/default/150000_300/agents/audio.md tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md
tests/fixtures/journal/20260307/default/150000_300/audio.json tests/fixtures/journal/chronicle/20260307/default/150000_300/audio.json
tests/fixtures/journal/20260307/default/150000_300/audio.jsonl tests/fixtures/journal/chronicle/20260307/default/150000_300/audio.jsonl
tests/fixtures/journal/20260307/default/150000_300/stream.json tests/fixtures/journal/chronicle/20260307/default/150000_300/stream.json
tests/fixtures/journal/20260307/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260307/indexer/transcripts.sqlite
tests/fixtures/journal/20260308/agents/flow.md tests/fixtures/journal/chronicle/20260308/agents/flow.md
tests/fixtures/journal/20260308/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md
tests/fixtures/journal/20260308/agents/meetings.md tests/fixtures/journal/chronicle/20260308/agents/meetings.md
tests/fixtures/journal/20260308/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md
tests/fixtures/journal/20260308/default/100000_300/audio.json tests/fixtures/journal/chronicle/20260308/default/100000_300/audio.json
tests/fixtures/journal/20260308/default/100000_300/audio.jsonl tests/fixtures/journal/chronicle/20260308/default/100000_300/audio.jsonl
tests/fixtures/journal/20260308/default/100000_300/stream.json tests/fixtures/journal/chronicle/20260308/default/100000_300/stream.json
tests/fixtures/journal/20260308/default/153000_300/agents/audio.md tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md
tests/fixtures/journal/20260308/default/153000_300/audio.json tests/fixtures/journal/chronicle/20260308/default/153000_300/audio.json
tests/fixtures/journal/20260308/default/153000_300/audio.jsonl tests/fixtures/journal/chronicle/20260308/default/153000_300/audio.jsonl
tests/fixtures/journal/20260308/default/153000_300/stream.json tests/fixtures/journal/chronicle/20260308/default/153000_300/stream.json
tests/fixtures/journal/20260308/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260308/indexer/transcripts.sqlite
tests/fixtures/journal/20260309/agents/flow.md tests/fixtures/journal/chronicle/20260309/agents/flow.md
tests/fixtures/journal/20260309/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md
tests/fixtures/journal/20260309/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md
tests/fixtures/journal/20260309/default/090000_300/audio.json tests/fixtures/journal/chronicle/20260309/default/090000_300/audio.json
tests/fixtures/journal/20260309/default/090000_300/audio.jsonl tests/fixtures/journal/chronicle/20260309/default/090000_300/audio.jsonl
tests/fixtures/journal/20260309/default/090000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260309/default/090000_300/monitor_1_diff.json
tests/fixtures/journal/20260309/default/090000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260309/default/090000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260309/default/090000_300/screen.jsonl tests/fixtures/journal/chronicle/20260309/default/090000_300/screen.jsonl
tests/fixtures/journal/20260309/default/090000_300/stream.json tests/fixtures/journal/chronicle/20260309/default/090000_300/stream.json
tests/fixtures/journal/20260309/default/133000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md
tests/fixtures/journal/20260309/default/133000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md
tests/fixtures/journal/20260309/default/133000_300/audio.json tests/fixtures/journal/chronicle/20260309/default/133000_300/audio.json
tests/fixtures/journal/20260309/default/133000_300/audio.jsonl tests/fixtures/journal/chronicle/20260309/default/133000_300/audio.jsonl
tests/fixtures/journal/20260309/default/133000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260309/default/133000_300/monitor_1_diff.json
tests/fixtures/journal/20260309/default/133000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260309/default/133000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260309/default/133000_300/screen.jsonl tests/fixtures/journal/chronicle/20260309/default/133000_300/screen.jsonl
tests/fixtures/journal/20260309/default/133000_300/stream.json tests/fixtures/journal/chronicle/20260309/default/133000_300/stream.json
tests/fixtures/journal/20260309/default/193000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md
tests/fixtures/journal/20260309/default/193000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md
tests/fixtures/journal/20260309/default/193000_300/audio.json tests/fixtures/journal/chronicle/20260309/default/193000_300/audio.json
tests/fixtures/journal/20260309/default/193000_300/audio.jsonl tests/fixtures/journal/chronicle/20260309/default/193000_300/audio.jsonl
tests/fixtures/journal/20260309/default/193000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260309/default/193000_300/monitor_1_diff.json
tests/fixtures/journal/20260309/default/193000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260309/default/193000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260309/default/193000_300/screen.jsonl tests/fixtures/journal/chronicle/20260309/default/193000_300/screen.jsonl
tests/fixtures/journal/20260309/default/193000_300/stream.json tests/fixtures/journal/chronicle/20260309/default/193000_300/stream.json
tests/fixtures/journal/20260309/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260309/indexer/transcripts.sqlite
tests/fixtures/journal/20260310/agents/flow.md tests/fixtures/journal/chronicle/20260310/agents/flow.md
tests/fixtures/journal/20260310/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md
tests/fixtures/journal/20260310/agents/meetings.md tests/fixtures/journal/chronicle/20260310/agents/meetings.md
tests/fixtures/journal/20260310/default/083000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md
tests/fixtures/journal/20260310/default/083000_300/agents/screen.md tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md
tests/fixtures/journal/20260310/default/083000_300/audio.json tests/fixtures/journal/chronicle/20260310/default/083000_300/audio.json
tests/fixtures/journal/20260310/default/083000_300/audio.jsonl tests/fixtures/journal/chronicle/20260310/default/083000_300/audio.jsonl
tests/fixtures/journal/20260310/default/083000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260310/default/083000_300/monitor_1_diff.json
tests/fixtures/journal/20260310/default/083000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260310/default/083000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260310/default/083000_300/screen.jsonl tests/fixtures/journal/chronicle/20260310/default/083000_300/screen.jsonl
tests/fixtures/journal/20260310/default/083000_300/stream.json tests/fixtures/journal/chronicle/20260310/default/083000_300/stream.json
tests/fixtures/journal/20260310/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md
tests/fixtures/journal/20260310/default/100000_300/audio.json tests/fixtures/journal/chronicle/20260310/default/100000_300/audio.json
tests/fixtures/journal/20260310/default/100000_300/audio.jsonl tests/fixtures/journal/chronicle/20260310/default/100000_300/audio.jsonl
tests/fixtures/journal/20260310/default/100000_300/stream.json tests/fixtures/journal/chronicle/20260310/default/100000_300/stream.json
tests/fixtures/journal/20260310/default/170000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md
tests/fixtures/journal/20260310/default/170000_300/agents/screen.md tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md
tests/fixtures/journal/20260310/default/170000_300/audio.json tests/fixtures/journal/chronicle/20260310/default/170000_300/audio.json
tests/fixtures/journal/20260310/default/170000_300/audio.jsonl tests/fixtures/journal/chronicle/20260310/default/170000_300/audio.jsonl
tests/fixtures/journal/20260310/default/170000_300/monitor_1_diff.json tests/fixtures/journal/chronicle/20260310/default/170000_300/monitor_1_diff.json
tests/fixtures/journal/20260310/default/170000_300/monitor_1_diff_box.json tests/fixtures/journal/chronicle/20260310/default/170000_300/monitor_1_diff_box.json
tests/fixtures/journal/20260310/default/170000_300/screen.jsonl tests/fixtures/journal/chronicle/20260310/default/170000_300/screen.jsonl
tests/fixtures/journal/20260310/default/170000_300/stream.json tests/fixtures/journal/chronicle/20260310/default/170000_300/stream.json
tests/fixtures/journal/20260310/indexer/transcripts.sqlite tests/fixtures/journal/chronicle/20260310/indexer/transcripts.sqlite
+7 -5
tests/test_activities.py
··· 638 638 639 639 def _setup_segment(tmpdir, day, segment, facet, state): 640 640 """Helper to create an activity_state.json file in a segment.""" 641 - agents_dir = Path(tmpdir) / day / "default" / segment / "agents" / facet 641 + agents_dir = ( 642 + Path(tmpdir) / "chronicle" / day / "default" / segment / "agents" / facet 643 + ) 642 644 agents_dir.mkdir(parents=True, exist_ok=True) 643 645 state_file = agents_dir / "activity_state.json" 644 646 state_file.write_text(json.dumps(state)) ··· 861 863 862 864 with tempfile.TemporaryDirectory() as tmpdir: 863 865 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 864 - (Path(tmpdir) / "20260209").mkdir() 866 + (Path(tmpdir) / "chronicle" / "20260209").mkdir(parents=True) 865 867 866 868 result = _walk_activity_segments( 867 869 "20260209", "work", "coding", "100000_300", "100500_300" ··· 878 880 with tempfile.TemporaryDirectory() as tmpdir: 879 881 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 880 882 881 - day_dir = Path(tmpdir) / "20260209" 882 - day_dir.mkdir() 883 + day_dir = Path(tmpdir) / "chronicle" / "20260209" 884 + day_dir.mkdir(parents=True) 883 885 (day_dir / "default" / "100000_300").mkdir(parents=True) 884 886 885 887 result = pre_process( ··· 1638 1640 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 1639 1641 1640 1642 # Create segment dir but no activity_state files 1641 - seg_dir = Path(tmpdir) / "20260209" / "default" / "100000_300" 1643 + seg_dir = Path(tmpdir) / "chronicle" / "20260209" / "default" / "100000_300" 1642 1644 seg_dir.mkdir(parents=True) 1643 1645 1644 1646 result = pre_process(
+37 -31
tests/test_activity_state.py
··· 54 54 55 55 try: 56 56 # Create day directory with segments 57 - day_dir = Path(tmpdir) / "20260130" 58 - day_dir.mkdir() 57 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 58 + day_dir.mkdir(parents=True) 59 59 (day_dir / "default" / "100000_300").mkdir(parents=True) 60 60 (day_dir / "default" / "110000_300").mkdir(parents=True) 61 61 (day_dir / "default" / "120000_300").mkdir(parents=True) ··· 90 90 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 91 91 92 92 try: 93 - day_dir = Path(tmpdir) / "20260130" 94 - day_dir.mkdir() 93 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 94 + day_dir.mkdir(parents=True) 95 95 (day_dir / "default" / "100000_300_audio").mkdir(parents=True) 96 96 (day_dir / "default" / "110000_300").mkdir(parents=True) 97 97 ··· 142 142 143 143 try: 144 144 # Create state file (new flat format) 145 - segment_dir = Path(tmpdir) / "20260130" / "default" / "100000_300" 145 + segment_dir = ( 146 + Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 147 + ) 146 148 segment_dir.mkdir(parents=True) 147 149 (segment_dir / "agents" / "work").mkdir(parents=True) 148 150 ··· 178 180 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 179 181 180 182 try: 181 - segment_dir = Path(tmpdir) / "20260130" / "default" / "100000_300" 183 + segment_dir = ( 184 + Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 185 + ) 182 186 segment_dir.mkdir(parents=True) 183 187 (segment_dir / "agents" / "work").mkdir(parents=True) 184 188 ··· 200 204 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 201 205 202 206 try: 203 - segment_dir = Path(tmpdir) / "20260130" / "default" / "100000_300" 207 + segment_dir = ( 208 + Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 209 + ) 204 210 segment_dir.mkdir(parents=True) 205 211 (segment_dir / "agents" / "work").mkdir(parents=True) 206 212 ··· 355 361 356 362 try: 357 363 # Create day and segments 358 - day_dir = Path(tmpdir) / "20260130" 359 - day_dir.mkdir() 364 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 365 + day_dir.mkdir(parents=True) 360 366 (day_dir / "default" / "100000_300").mkdir(parents=True) 361 367 (day_dir / "default" / "100000_300" / "agents" / "work").mkdir( 362 368 parents=True ··· 475 481 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 476 482 477 483 try: 478 - day_dir = Path(tmpdir) / "20260130" 479 - day_dir.mkdir() 484 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 485 + day_dir.mkdir(parents=True) 480 486 481 487 # Previous segment with active meeting 482 488 prev_dir = day_dir / "default" / "100000_300" ··· 533 539 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 534 540 535 541 try: 536 - day_dir = Path(tmpdir) / "20260130" 537 - day_dir.mkdir() 542 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 543 + day_dir.mkdir(parents=True) 538 544 539 545 # Previous segment with active meeting 540 546 prev_dir = day_dir / "default" / "100000_300" ··· 657 663 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 658 664 659 665 try: 660 - day_dir = Path(tmpdir) / "20260130" 661 - day_dir.mkdir() 666 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 667 + day_dir.mkdir(parents=True) 662 668 663 669 # Previous segment — email already ended 664 670 prev_dir = day_dir / "default" / "100000_300" ··· 713 719 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 714 720 715 721 try: 716 - day_dir = Path(tmpdir) / "20260130" 717 - day_dir.mkdir() 722 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 723 + day_dir.mkdir(parents=True) 718 724 719 725 # Previous segment — email ended with different description 720 726 prev_dir = day_dir / "default" / "100000_300" ··· 795 801 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 796 802 797 803 try: 798 - day_dir = Path(tmpdir) / "20260130" 799 - day_dir.mkdir() 804 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 805 + day_dir.mkdir(parents=True) 800 806 801 807 prev_dir = day_dir / "default" / "100000_300" 802 808 prev_dir.mkdir(parents=True) ··· 916 922 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 917 923 918 924 try: 919 - day_dir = Path(tmpdir) / "20260130" 920 - day_dir.mkdir() 925 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 926 + day_dir.mkdir(parents=True) 921 927 922 928 prev_dir = day_dir / "default" / "100000_300" 923 929 prev_dir.mkdir(parents=True) ··· 973 979 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 974 980 975 981 try: 976 - day_dir = Path(tmpdir) / "20260130" 977 - day_dir.mkdir() 982 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 983 + day_dir.mkdir(parents=True) 978 984 979 985 prev_dir = day_dir / "default" / "100000_300" 980 986 prev_dir.mkdir(parents=True) ··· 1058 1064 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 1059 1065 1060 1066 try: 1061 - day_dir = Path(tmpdir) / "20260130" 1062 - day_dir.mkdir() 1067 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 1068 + day_dir.mkdir(parents=True) 1063 1069 1064 1070 prev_dir = day_dir / "default" / "100000_300" 1065 1071 prev_dir.mkdir(parents=True) ··· 1113 1119 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 1114 1120 1115 1121 try: 1116 - day_dir = Path(tmpdir) / "20260130" 1117 - day_dir.mkdir() 1122 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 1123 + day_dir.mkdir(parents=True) 1118 1124 1119 1125 prev_dir = day_dir / "default" / "100000_300" 1120 1126 prev_dir.mkdir(parents=True) ··· 1233 1239 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 1234 1240 1235 1241 try: 1236 - day_dir = Path(tmpdir) / "20260130" 1237 - day_dir.mkdir() 1242 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 1243 + day_dir.mkdir(parents=True) 1238 1244 1239 1245 prev_dir = day_dir / "default" / "100000_300" 1240 1246 prev_dir.mkdir(parents=True) ··· 1295 1301 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = tmpdir 1296 1302 1297 1303 try: 1298 - day_dir = Path(tmpdir) / "20260130" 1299 - day_dir.mkdir() 1304 + day_dir = Path(tmpdir) / "chronicle" / "20260130" 1305 + day_dir.mkdir(parents=True) 1300 1306 1301 1307 prev_dir = day_dir / "default" / "100000_300" 1302 1308 prev_dir.mkdir(parents=True)
+2 -2
tests/test_app_sol.py
··· 256 256 state.journal_root = str(tmp_path) 257 257 258 258 # Create test files 259 - day_dir = tmp_path / "20260214" 260 - day_dir.mkdir() 259 + day_dir = tmp_path / "chronicle" / "20260214" 260 + day_dir.mkdir(parents=True) 261 261 (day_dir / "agents" / "flow.md").parent.mkdir(parents=True) 262 262 (day_dir / "agents" / "flow.md").write_text("# Day agent output") 263 263
+1 -1
tests/test_call.py
··· 10 10 import typer 11 11 from typer.testing import CliRunner 12 12 13 + from tests.conftest import copytree_tracked 13 14 from think.call import call_app 14 15 from think.utils import resolve_sol_day, resolve_sol_facet, resolve_sol_segment 15 - from tests.conftest import copytree_tracked 16 16 17 17 runner = CliRunner() 18 18
+2 -2
tests/test_cluster_full.py
··· 5 5 import os 6 6 from pathlib import Path 7 7 8 - from think.utils import day_path 9 8 from tests.conftest import copytree_tracked 9 + from think.utils import day_path 10 10 11 11 FIXTURES = Path("tests/fixtures") 12 12 ··· 14 14 def copy_day(tmp_path: Path) -> Path: 15 15 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 16 16 dest = day_path("20240101") 17 - src = FIXTURES / "journal" / "20240101" 17 + src = FIXTURES / "journal" / "chronicle" / "20240101" 18 18 copytree_tracked(src, dest) 19 19 return dest 20 20
+1 -1
tests/test_content_api.py
··· 92 92 def test_content_lazy_backfill(tmp_path): 93 93 journal_root = tmp_path 94 94 import_dir = journal_root / "imports" / "20260101_120000" 95 - seg_dir = journal_root / "20260101" / "import.chatgpt" / "120000_300" 95 + seg_dir = journal_root / "chronicle" / "20260101" / "import.chatgpt" / "120000_300" 96 96 import_dir.mkdir(parents=True) 97 97 seg_dir.mkdir(parents=True) 98 98
+1 -1
tests/test_content_manifest.py
··· 51 51 def test_generate_content_manifest_from_segments(tmp_path): 52 52 journal_root = tmp_path 53 53 import_dir = journal_root / "imports" / "20260101_090000" 54 - segment_dir = journal_root / "20260101" / "import.ics" / "090000_300" 54 + segment_dir = journal_root / "chronicle" / "20260101" / "import.ics" / "090000_300" 55 55 segment_dir.mkdir(parents=True) 56 56 import_dir.mkdir(parents=True) 57 57
+3 -1
tests/test_conversation.py
··· 11 11 12 12 13 13 @pytest.fixture 14 - def journal_dir(tmp_path): 14 + def journal_dir(tmp_path, monkeypatch): 15 15 """Create a temporary journal directory.""" 16 16 journal = tmp_path / "journal" 17 17 journal.mkdir() 18 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 18 19 with mock.patch("think.conversation.get_journal", return_value=str(journal)): 19 20 yield journal 20 21 ··· 79 80 time_key = datetime.fromtimestamp(ts / 1000).strftime("%H%M%S") 80 81 md_path = ( 81 82 journal_dir 83 + / "chronicle" 82 84 / day 83 85 / "conversation" 84 86 / f"{time_key}_1"
+1 -5
tests/test_dream_activity.py
··· 554 554 555 555 sm = ActivityStateMachine() 556 556 sm.update(self._sense(content_type="coding"), "090000_300", "20260304") 557 - changes = sm.update( 558 - self._sense(content_type="meeting"), "090500_300", "20260304" 559 - ) 557 + sm.update(self._sense(content_type="meeting"), "090500_300", "20260304") 560 558 561 - ended = [c for c in changes if c.get("state") == "ended"] 562 559 rec = sm.get_completed_activities()[0] 563 560 564 561 # Write once ··· 732 729 733 730 def test_created_at_passes_cutoff_filter(self, monkeypatch): 734 731 """Simulate routes.py filtering — recent records pass, old records don't.""" 735 - import time 736 732 from datetime import datetime, timedelta 737 733 738 734 from think.activities import append_activity_record, load_activity_records
+1 -1
tests/test_dream_full.py
··· 53 53 mod = importlib.import_module("think.dream") 54 54 55 55 # Create segment directory 56 - segment_dir = journal_copy / "20240101" / "default" / "120000_300" 56 + segment_dir = journal_copy / "chronicle" / "20240101" / "default" / "120000_300" 57 57 segment_dir.mkdir(parents=True, exist_ok=True) 58 58 59 59 commands_run = []
+3 -3
tests/test_dream_segment.py
··· 14 14 def segment_dir(tmp_path, monkeypatch): 15 15 """Create a temporary journal with a segment directory.""" 16 16 journal = tmp_path / "journal" 17 - day_dir = journal / "20240115" 17 + day_dir = journal / "chronicle" / "20240115" 18 18 segment_path = day_dir / "default" / "120000_300" 19 19 segment_path.mkdir(parents=True) 20 20 (segment_path / "agents").mkdir(parents=True) ··· 226 226 227 227 # Verify activity state persisted even on idle path 228 228 activity_state_path = ( 229 - segment_dir.parent.parent.parent / "awareness" / "activity_state.json" 229 + segment_dir.parents[3] / "awareness" / "activity_state.json" 230 230 ) 231 231 assert activity_state_path.exists() 232 232 state_data = json.loads(activity_state_path.read_text()) ··· 566 566 } 567 567 ] 568 568 activity_state_path = ( 569 - segment_dir.parent.parent.parent / "awareness" / "activity_state.json" 569 + segment_dir.parents[3] / "awareness" / "activity_state.json" 570 570 ) 571 571 assert activity_state_path.exists() 572 572 state_data = json.loads(activity_state_path.read_text())
+2 -2
tests/test_entities.py
··· 1742 1742 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 1743 1743 1744 1744 # Create a knowledge graph file 1745 - day_dir = tmp_path / "20260108" / "agents" 1745 + day_dir = tmp_path / "chronicle" / "20260108" / "agents" 1746 1746 day_dir.mkdir(parents=True) 1747 1747 1748 1748 kg_content = """# Knowledge Graph Report ··· 1790 1790 """Test parsing returns empty list for empty KG.""" 1791 1791 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 1792 1792 1793 - day_dir = tmp_path / "20260108" / "agents" 1793 + day_dir = tmp_path / "chronicle" / "20260108" / "agents" 1794 1794 day_dir.mkdir(parents=True) 1795 1795 (day_dir / "knowledge_graph.md").write_text("") 1796 1796
+3 -3
tests/test_export.py
··· 16 16 17 17 18 18 def _setup_journal(tmp_path, *, include_stream_json: bool = False): 19 - first_dir = tmp_path / "20260413" / "laptop" / "143022_300" 20 - second_dir = tmp_path / "20260413" / "laptop" / "150000_600" 19 + first_dir = tmp_path / "chronicle" / "20260413" / "laptop" / "143022_300" 20 + second_dir = tmp_path / "chronicle" / "20260413" / "laptop" / "150000_600" 21 21 first_dir.mkdir(parents=True) 22 22 second_dir.mkdir(parents=True) 23 23 ··· 324 324 journal = _setup_journal(tmp_path) 325 325 _set_journal_override(monkeypatch, journal) 326 326 327 - second_dir = journal / "20260413" / "laptop" / "150000_600" 327 + second_dir = journal / "chronicle" / "20260413" / "laptop" / "150000_600" 328 328 for file_path in second_dir.iterdir(): 329 329 file_path.unlink() 330 330 second_dir.rmdir()
+22 -6
tests/test_export_integration.py
··· 208 208 def _setup_segments( 209 209 journal_root: Path, *, day: str = "20260413", include_default: bool = False 210 210 ) -> list[str]: 211 - segment_dir = journal_root / day / "laptop" / "143022_300" 211 + chronicle_day = journal_root / "chronicle" / day 212 + segment_dir = chronicle_day / "laptop" / "143022_300" 212 213 _write_bytes(segment_dir / "audio.flac", b"audio-data") 213 214 _write_bytes(segment_dir / "transcript.jsonl", b'{"text":"hello"}\n') 214 215 if include_default: 215 - default_segment_dir = journal_root / day / "180000_300" 216 + default_segment_dir = chronicle_day / "180000_300" 216 217 _write_bytes(default_segment_dir / "audio.flac", b"default-audio") 217 218 return [day] 218 219 ··· 317 318 assert config_result == ExportResult(area="config", staged=1) 318 319 319 320 assert ( 320 - env["target"] / "20260413" / "laptop" / "143022_300" / "audio.flac" 321 + env["target"] 322 + / "chronicle" 323 + / "20260413" 324 + / "laptop" 325 + / "143022_300" 326 + / "audio.flac" 321 327 ).exists() 322 328 assert (env["target"] / "entities" / "source_entity" / "entity.json").exists() 323 329 assert (env["target"] / "imports" / "20260101_090000" / "import.json").exists() ··· 379 385 380 386 assert first.sent == 2 381 387 assert ( 382 - env["target"] / "20260413" / "_default" / "180000_300" / "audio.flac" 388 + env["target"] 389 + / "chronicle" 390 + / "20260413" 391 + / "_default" 392 + / "180000_300" 393 + / "audio.flac" 383 394 ).exists() 384 395 assert second.sent == 0 385 396 assert second.skipped == 2 ··· 395 406 396 407 assert result.sent == 1 397 408 assert ( 398 - env["target"] / "20260413" / "laptop" / "143022_300" / "transcript.jsonl" 409 + env["target"] 410 + / "chronicle" 411 + / "20260413" 412 + / "laptop" 413 + / "143022_300" 414 + / "transcript.jsonl" 399 415 ).exists() 400 416 401 417 ··· 612 628 assert entity_result.sent == 1 613 629 assert facet_result.sent == 1 614 630 assert config_result.staged == 1 615 - assert not (env["target"] / "20260413").exists() 631 + assert not (env["target"] / "chronicle" / "20260413").exists() 616 632 assert not (env["target"] / "entities" / "source_entity" / "entity.json").exists()
+4 -4
tests/test_facets.py
··· 343 343 def test_get_active_facets_from_segment_facets(monkeypatch, tmp_path): 344 344 """Test get_active_facets() returns facets from segment facets.json files.""" 345 345 journal = tmp_path / "journal" 346 - day_dir = journal / "20240115" 346 + day_dir = journal / "chronicle" / "20240115" 347 347 348 348 # Create segment with facets.json containing two facets (stream layout) 349 349 seg1 = day_dir / "archon" / "100000_300" / "agents" ··· 379 379 def test_get_active_facets_empty_segments(monkeypatch, tmp_path): 380 380 """Test get_active_facets() with segments that have empty facets.json.""" 381 381 journal = tmp_path / "journal" 382 - day_dir = journal / "20240115" 382 + day_dir = journal / "chronicle" / "20240115" 383 383 384 384 # Segment with empty facets array (stream layout) 385 385 seg1 = day_dir / "archon" / "100000_300" / "agents" ··· 401 401 def test_get_active_facets_no_segments(monkeypatch, tmp_path): 402 402 """Test get_active_facets() when day directory has no segments.""" 403 403 journal = tmp_path / "journal" 404 - (journal / "20240115").mkdir(parents=True) 404 + (journal / "chronicle" / "20240115").mkdir(parents=True) 405 405 406 406 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 407 407 ··· 425 425 def test_get_active_facets_malformed_json(monkeypatch, tmp_path): 426 426 """Test get_active_facets() skips malformed facets.json gracefully.""" 427 427 journal = tmp_path / "journal" 428 - day_dir = journal / "20240115" 428 + day_dir = journal / "chronicle" / "20240115" 429 429 430 430 # Malformed JSON segment (stream layout) 431 431 seg1 = day_dir / "archon" / "100000_300" / "agents"
+8 -6
tests/test_formatters.py
··· 89 89 90 90 path = ( 91 91 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 92 - / "20240101/default/123456_300/audio.jsonl" 92 + / "chronicle/20240101/default/123456_300/audio.jsonl" 93 93 ) 94 94 entries = load_jsonl(path) 95 95 ··· 144 144 145 145 path = ( 146 146 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 147 - / "20240102/default/234567_300/screen.jsonl" 147 + / "chronicle/20240102/default/234567_300/screen.jsonl" 148 148 ) 149 149 chunks, meta = format_file(path) 150 150 ··· 161 161 162 162 path = ( 163 163 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 164 - / "20240101/default/123456_300/audio.jsonl" 164 + / "chronicle/20240101/default/123456_300/audio.jsonl" 165 165 ) 166 166 chunks, meta = format_file(path) 167 167 ··· 471 471 472 472 path = ( 473 473 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 474 - / "20240101/default/123456_300/audio.jsonl" 474 + / "chronicle/20240101/default/123456_300/audio.jsonl" 475 475 ) 476 476 metadata, entries, formatted_text = load_transcript(path) 477 477 ··· 1281 1281 from think.formatters import format_file 1282 1282 1283 1283 path = ( 1284 - Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) / "20240101/agents/flow.md" 1284 + Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 1285 + / "chronicle/20240101/agents/flow.md" 1285 1286 ) 1286 1287 chunks, meta = format_file(path) 1287 1288 ··· 1294 1295 from think.formatters import load_markdown 1295 1296 1296 1297 path = ( 1297 - Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) / "20240101/agents/flow.md" 1298 + Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 1299 + / "chronicle/20240101/agents/flow.md" 1298 1300 ) 1299 1301 text = load_markdown(path) 1300 1302
+5 -3
tests/test_generate_full.py
··· 15 15 import os 16 16 from pathlib import Path 17 17 18 - from think.utils import day_path 19 18 from tests.conftest import copytree_tracked 19 + from think.utils import day_path 20 20 21 21 FIXTURES = Path("tests/fixtures") 22 22 ··· 24 24 def copy_day(tmp_path: Path) -> Path: 25 25 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 26 26 dest = day_path("20240101") 27 - src = FIXTURES / "journal" / "20240101" 27 + src = FIXTURES / "journal" / "chronicle" / "20240101" 28 28 copytree_tracked(src, dest) 29 29 return dest 30 30 ··· 170 170 assert len(finish_events) == 1 171 171 172 172 # Read captured context 173 - captured_path = tmp_path / "20240101" / "agents" / "context_captured.json" 173 + captured_path = ( 174 + tmp_path / "chronicle" / "20240101" / "agents" / "context_captured.json" 175 + ) 174 176 captured = json.loads(captured_path.read_text()) 175 177 176 178 assert captured["day"] == "20240101"
+2 -2
tests/test_generate_scan_day.py
··· 5 5 import os 6 6 from pathlib import Path 7 7 8 - from think.utils import day_path 9 8 from tests.conftest import copytree_tracked 9 + from think.utils import day_path 10 10 11 11 FIXTURES = Path("tests/fixtures") 12 12 ··· 14 14 def copy_day(tmp_path: Path) -> Path: 15 15 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 16 16 dest = day_path("20240101") 17 - src = FIXTURES / "journal" / "20240101" 17 + src = FIXTURES / "journal" / "chronicle" / "20240101" 18 18 copytree_tracked(src, dest) 19 19 agents_dir = dest / "agents" 20 20 agents_dir.mkdir(exist_ok=True) # Allow existing directory
+9 -8
tests/test_importer.py
··· 269 269 mod = importlib.import_module("think.importers.shared") 270 270 271 271 json_path = mod.write_segment( 272 - str(tmp_path / "20240101"), 272 + str(tmp_path / "chronicle" / "20240101"), 273 273 "import.text", 274 274 "120000_300", 275 275 [{"start": "00:00:00", "speaker": "Alice", "text": "Hello"}], ··· 284 284 assert ( 285 285 written 286 286 == tmp_path 287 + / "chronicle" 287 288 / "20240101" 288 289 / "import.text" 289 290 / "120000_300" ··· 720 721 audio_file = tmp_path / "test.mp3" 721 722 audio_file.write_bytes(b"fake audio content") 722 723 723 - day_dir = tmp_path / "20240101" 724 - day_dir.mkdir() 724 + day_dir = tmp_path / "chronicle" / "20240101" 725 + day_dir.mkdir(parents=True) 725 726 726 727 base_dt = dt.datetime(2024, 1, 1, 12, 0, 0) 727 728 ··· 773 774 audio_file = tmp_path / "test.mp3" 774 775 audio_file.write_bytes(b"fake audio content") 775 776 776 - day_dir = tmp_path / "20240101" 777 - day_dir.mkdir() 777 + day_dir = tmp_path / "chronicle" / "20240101" 778 + day_dir.mkdir(parents=True) 778 779 779 780 base_dt = dt.datetime(2024, 1, 1, 12, 0, 0) 780 781 ··· 840 841 assert "2 lines" in captured.out 841 842 842 843 assert not (tmp_path / "imports").exists() 843 - assert not (tmp_path / "20240101").exists() 844 + assert not (tmp_path / "chronicle" / "20240101").exists() 844 845 845 846 846 847 def test_importer_dry_run_audio(tmp_path, monkeypatch, capsys): ··· 879 880 assert "120500_300" in captured.out 880 881 881 882 assert not (tmp_path / "imports").exists() 882 - assert not (tmp_path / "20240101").exists() 883 + assert not (tmp_path / "chronicle" / "20240101").exists() 883 884 assert callosum_cls.call_count == 0 884 885 885 886 ··· 909 910 assert "Content:" in captured.out 910 911 911 912 assert not (tmp_path / "imports").exists() 912 - assert not (tmp_path / "20240315").exists() 913 + assert not (tmp_path / "chronicle" / "20240315").exists() 913 914 914 915 915 916 def test_file_importer_without_timestamp(tmp_path, monkeypatch, capsys):
+19 -9
tests/test_importer_documents.py
··· 75 75 pdf.write_bytes(b"%PDF-1.4") 76 76 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 77 77 monkeypatch.setattr(mod, "PdfReader", MockPdfReader) 78 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 78 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 79 79 monkeypatch.setattr( 80 80 mod, 81 81 "write_content_manifest", ··· 87 87 pdf, tmp_path, facet="work", import_id="20260115_120000" 88 88 ) 89 89 90 - seg_dir = tmp_path / "20260115" / "import.document" / "120000_0" 90 + seg_dir = tmp_path / "chronicle" / "20260115" / "import.document" / "120000_0" 91 91 md_path = seg_dir / "document_transcript.md" 92 92 93 93 assert result.entries_written == 1 ··· 107 107 pdf.write_bytes(b"%PDF-1.4 fake") 108 108 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 109 109 monkeypatch.setattr(mod, "PdfReader", MockPdfReader) 110 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 110 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 111 111 monkeypatch.setattr( 112 112 mod, 113 113 "write_content_manifest", ··· 117 117 118 118 result = mod.importer.process(pdf, tmp_path, import_id="20260115_120000") 119 119 120 - copied = tmp_path / "20260115" / "import.document" / "120000_0" / "original.pdf" 120 + copied = ( 121 + tmp_path 122 + / "chronicle" 123 + / "20260115" 124 + / "import.document" 125 + / "120000_0" 126 + / "original.pdf" 127 + ) 121 128 assert copied.exists() 122 129 assert copied.read_bytes() == b"%PDF-1.4 fake" 123 130 assert str(copied) not in result.files_created ··· 136 143 pdf.write_bytes(b"%PDF-1.4") 137 144 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 138 145 monkeypatch.setattr(mod, "PdfReader", ScannedReader) 139 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 146 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 140 147 monkeypatch.setattr( 141 148 mod, 142 149 "write_content_manifest", ··· 157 164 assert result.errors == [] 158 165 md_path = ( 159 166 tmp_path 167 + / "chronicle" 160 168 / "20260115" 161 169 / "import.document" 162 170 / "120000_0" ··· 178 186 pdf.write_bytes(b"%PDF-1.4") 179 187 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 180 188 monkeypatch.setattr(mod, "PdfReader", ScannedReader) 181 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 189 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 182 190 monkeypatch.setattr( 183 191 mod, 184 192 "write_content_manifest", ··· 198 206 199 207 md_path = ( 200 208 tmp_path 209 + / "chronicle" 201 210 / "20260115" 202 211 / "import.document" 203 212 / "120000_0" ··· 219 228 pdf.write_bytes(b"%PDF-1.4") 220 229 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 221 230 monkeypatch.setattr(mod, "PdfReader", ScannedReader) 222 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 231 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 223 232 monkeypatch.setattr( 224 233 mod, 225 234 "write_content_manifest", ··· 246 255 ] 247 256 md_path = ( 248 257 tmp_path 258 + / "chronicle" 249 259 / "20260115" 250 260 / "import.document" 251 261 / "120000_0" ··· 262 272 pdf_b.write_bytes(b"%PDF-1.4") 263 273 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 264 274 monkeypatch.setattr(mod, "PdfReader", MockPdfReader) 265 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 275 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 266 276 monkeypatch.setattr( 267 277 mod, 268 278 "write_content_manifest", ··· 293 303 pdf.write_bytes(b"%PDF-1.4") 294 304 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 295 305 monkeypatch.setattr(mod, "PdfReader", EntityReader) 296 - monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / day) 306 + monkeypatch.setattr(mod, "day_path", lambda day: tmp_path / "chronicle" / day) 297 307 monkeypatch.setattr( 298 308 mod, 299 309 "write_content_manifest",
+11 -2
tests/test_importer_granola.py
··· 523 523 import glob 524 524 525 525 segments = glob.glob( 526 - str(tmp_path / "*/import.granola/*/conversation_transcript.jsonl") 526 + str( 527 + tmp_path 528 + / "chronicle" 529 + / "*" 530 + / "import.granola" 531 + / "*" 532 + / "conversation_transcript.jsonl" 533 + ) 527 534 ) 528 535 assert len(segments) >= 1 529 536 ··· 545 552 assert entry["source"] == "import" 546 553 547 554 # Check source.md was copied 548 - source_files = glob.glob(str(tmp_path / "*/import.granola/*/source.md")) 555 + source_files = glob.glob( 556 + str(tmp_path / "chronicle" / "*" / "import.granola" / "*" / "source.md") 557 + ) 549 558 assert len(source_files) == 1 # only in first segment 550 559 551 560
+30 -3
tests/test_importer_obsidian_sync.py
··· 105 105 assert result["downloaded"] >= 1 106 106 assert result["imported"] >= 1 107 107 108 - segments = glob.glob(str(tmp_path / "*/import.obsidian/*/note_transcript.md")) 108 + segments = glob.glob( 109 + str( 110 + tmp_path 111 + / "chronicle" 112 + / "*" 113 + / "import.obsidian" 114 + / "*" 115 + / "note_transcript.md" 116 + ) 117 + ) 109 118 assert len(segments) >= 1 110 119 111 120 state = load_sync_state(tmp_path, "obsidian") ··· 126 135 first = backend.sync(tmp_path, source_path=vault, dry_run=False) 127 136 assert first["downloaded"] == 1 128 137 first_segments = sorted( 129 - glob.glob(str(tmp_path / "*/import.obsidian/*/note_transcript.md")) 138 + glob.glob( 139 + str( 140 + tmp_path 141 + / "chronicle" 142 + / "*" 143 + / "import.obsidian" 144 + / "*" 145 + / "note_transcript.md" 146 + ) 147 + ) 130 148 ) 131 149 assert len(first_segments) == 1 132 150 ··· 135 153 assert second["downloaded"] == 1 136 154 137 155 all_segments = sorted( 138 - glob.glob(str(tmp_path / "*/import.obsidian/*/note_transcript.md")) 156 + glob.glob( 157 + str( 158 + tmp_path 159 + / "chronicle" 160 + / "*" 161 + / "import.obsidian" 162 + / "*" 163 + / "note_transcript.md" 164 + ) 165 + ) 139 166 ) 140 167 assert len(all_segments) == 2 141 168 assert first_segments[0] in all_segments
+37 -13
tests/test_journal_index.py
··· 10 10 11 11 import pytest 12 12 13 + from tests.conftest import copytree_tracked 13 14 from think.indexer import sanitize_fts_query 14 15 from think.indexer.journal import ( 15 16 extract_temporal_references, 16 17 get_journal_index, 17 18 search_journal, 18 19 ) 19 - from tests.conftest import copytree_tracked 20 20 21 21 22 22 class TestSanitizeFtsQuery: ··· 279 279 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(journal) 280 280 281 281 # Create daily insight 282 - day = journal / "20240101" 283 - day.mkdir() 282 + day = journal / "chronicle" / "20240101" 283 + day.mkdir(parents=True) 284 284 agents_dir = day / "agents" 285 285 agents_dir.mkdir() 286 286 (agents_dir / "flow.md").write_text("# Flow Summary\n\nWorked on project alpha.\n") ··· 908 908 # Create content for today (which is in light scan scope) 909 909 today = datetime.now().strftime("%Y%m%d") 910 910 day_dir = journal / today 911 - day_dir.mkdir() 911 + day_dir.mkdir(parents=True) 912 912 agents_dir = day_dir / "agents" 913 913 agents_dir.mkdir() 914 914 output_file = agents_dir / "flow.md" ··· 941 941 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(journal) 942 942 943 943 # Create historical day content 944 - day_dir = journal / "20200101" 945 - day_dir.mkdir() 944 + day_dir = journal / "chronicle" / "20200101" 945 + day_dir.mkdir(parents=True) 946 946 agents_dir = day_dir / "agents" 947 947 agents_dir.mkdir() 948 948 output_file = agents_dir / "flow.md" ··· 976 976 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(journal) 977 977 978 978 # Create historical day content 979 - day_dir = journal / "20200101" 980 - day_dir.mkdir() 979 + day_dir = journal / "chronicle" / "20200101" 980 + day_dir.mkdir(parents=True) 981 981 agents_dir = day_dir / "agents" 982 982 agents_dir.mkdir() 983 983 output_file = agents_dir / "flow.md" ··· 1019 1019 """Test indexing with absolute path.""" 1020 1020 from think.indexer.journal import index_file, search_journal 1021 1021 1022 - abs_path = str(journal_fixture / "20240101" / "agents" / "flow.md") 1022 + abs_path = str(journal_fixture / "chronicle" / "20240101" / "agents" / "flow.md") 1023 1023 result = index_file(str(journal_fixture), abs_path, verbose=True) 1024 1024 assert result is True 1025 1025 ··· 1028 1028 assert total >= 1 1029 1029 1030 1030 1031 + def test_scan_journal_never_stores_chronicle_prefix(journal_fixture): 1032 + """Chronicle is an on-disk prefix only, never a stored relative path.""" 1033 + from think.indexer.journal import scan_journal 1034 + 1035 + scan_journal(str(journal_fixture), full=True) 1036 + 1037 + conn, _ = get_journal_index(str(journal_fixture)) 1038 + try: 1039 + assert ( 1040 + conn.execute( 1041 + "SELECT count(*) FROM files WHERE path LIKE 'chronicle/%'" 1042 + ).fetchone()[0] 1043 + == 0 1044 + ) 1045 + assert ( 1046 + conn.execute( 1047 + "SELECT count(*) FROM chunks WHERE path LIKE 'chronicle/%'" 1048 + ).fetchone()[0] 1049 + == 0 1050 + ) 1051 + finally: 1052 + conn.close() 1053 + 1054 + 1031 1055 def test_index_file_updates_existing(journal_fixture): 1032 1056 """Test that re-indexing a file replaces existing chunks.""" 1033 1057 from think.indexer.journal import index_file, search_journal ··· 1072 1096 from think.indexer.journal import index_file 1073 1097 1074 1098 # Create a file with no formatter (e.g., .txt) 1075 - txt_file = journal_fixture / "20240101" / "notes.txt" 1099 + txt_file = journal_fixture / "chronicle" / "20240101" / "notes.txt" 1076 1100 txt_file.write_text("Just some text notes.\n") 1077 1101 1078 1102 with pytest.raises(ValueError, match="No formatter found"): ··· 1088 1112 from think.streams import write_segment_stream 1089 1113 1090 1114 # Create a segment with stream marker 1091 - seg_dir = tmp_path / "20240101" / "default" / "123456_300" 1115 + seg_dir = tmp_path / "chronicle" / "20240101" / "default" / "123456_300" 1092 1116 seg_dir.mkdir(parents=True) 1093 1117 write_segment_stream(seg_dir, "archon", None, None, 1) 1094 1118 ··· 1113 1137 """_extract_stream returns None when stream.json doesn't exist.""" 1114 1138 from think.indexer.journal import _extract_stream 1115 1139 1116 - seg_dir = tmp_path / "20240101" / "default" / "123456_300" 1140 + seg_dir = tmp_path / "chronicle" / "20240101" / "default" / "123456_300" 1117 1141 seg_dir.mkdir(parents=True) 1118 1142 1119 1143 result = _extract_stream( ··· 1443 1467 conn.close() 1444 1468 assert initial == 45 1445 1469 1446 - kg_file = dst / "20240101" / "agents" / "knowledge_graph.md" 1470 + kg_file = dst / "chronicle" / "20240101" / "agents" / "knowledge_graph.md" 1447 1471 kg_file.unlink() 1448 1472 1449 1473 scan_journal(j, full=True)
+12 -8
tests/test_journal_merge.py
··· 76 76 encoding="utf-8", 77 77 ) 78 78 79 - (target / "20260101" / "120000_60").mkdir(parents=True) 80 - (target / "20260101" / "120000_60" / "audio.jsonl").write_text( 79 + (target / "chronicle" / "20260101" / "120000_60").mkdir(parents=True) 80 + (target / "chronicle" / "20260101" / "120000_60" / "audio.jsonl").write_text( 81 81 '{"audio": "target-existing-segment"}\n', 82 82 encoding="utf-8", 83 83 ) ··· 138 138 result = runner.invoke(call_app, ["journal", "merge", str(paths["source"])]) 139 139 140 140 assert result.exit_code == 0 141 - assert (paths["target"] / "20260101" / "143022_300" / "audio.jsonl").exists() 141 + assert ( 142 + paths["target"] / "chronicle" / "20260101" / "143022_300" / "audio.jsonl" 143 + ).exists() 142 144 143 145 144 146 def test_segment_skip(merge_journals_fixture, monkeypatch): ··· 148 150 result = runner.invoke(call_app, ["journal", "merge", str(paths["source"])]) 149 151 150 152 assert result.exit_code == 0 151 - assert (paths["target"] / "20260101" / "120000_60" / "audio.jsonl").read_text( 152 - encoding="utf-8" 153 - ) == '{"audio": "target-existing-segment"}\n' 153 + assert ( 154 + paths["target"] / "chronicle" / "20260101" / "120000_60" / "audio.jsonl" 155 + ).read_text(encoding="utf-8") == '{"audio": "target-existing-segment"}\n' 154 156 155 157 156 158 def test_entity_create(merge_journals_fixture, monkeypatch): ··· 576 578 577 579 assert result.exit_code == 0 578 580 assert "Would merge:" in result.output 579 - assert not (paths["target"] / "20260101" / "143022_300").exists() 581 + assert not (paths["target"] / "chronicle" / "20260101" / "143022_300").exists() 580 582 assert not (paths["target"] / "entities" / "bob_smith").exists() 581 583 assert not (paths["target"] / "facets" / "work").exists() 582 584 assert not (paths["target"] / "imports" / "20260101_120000").exists() ··· 632 634 result = runner.invoke(call_app, ["journal", "merge", str(paths["source"])]) 633 635 634 636 assert result.exit_code == 0 635 - assert (paths["target"] / "20260101" / "143022_300" / "audio.jsonl").exists() 637 + assert ( 638 + paths["target"] / "chronicle" / "20260101" / "143022_300" / "audio.jsonl" 639 + ).exists() 636 640 assert (paths["target"] / "entities" / "bob_smith" / "entity.json").exists() 637 641 assert "1 errors:" in result.output 638 642
+14 -14
tests/test_journal_stats.py
··· 8 8 def test_scan_day(tmp_path, monkeypatch): 9 9 stats_mod = importlib.import_module("think.journal_stats") 10 10 journal = tmp_path 11 - day = journal / "20240101" 12 - day.mkdir() 11 + day = journal / "chronicle" / "20240101" 12 + day.mkdir(parents=True) 13 13 14 14 # Create an audio jsonl file in segment directory (already processed) 15 15 ts_dir = day / "default" / "123456_300" ··· 68 68 def test_token_usage(tmp_path, monkeypatch): 69 69 stats_mod = importlib.import_module("think.journal_stats") 70 70 journal = tmp_path 71 - day1 = journal / "20240101" 72 - day1.mkdir() 73 - day2 = journal / "20240102" 74 - day2.mkdir() 71 + day1 = journal / "chronicle" / "20240101" 72 + day1.mkdir(parents=True) 73 + day2 = journal / "chronicle" / "20240102" 74 + day2.mkdir(parents=True) 75 75 76 76 # Create tokens directory with test token files 77 77 tokens_dir = journal / "tokens" ··· 185 185 """Test that per-day caching works correctly.""" 186 186 stats_mod = importlib.import_module("think.journal_stats") 187 187 journal = tmp_path 188 - day = journal / "20240101" 189 - day.mkdir() 188 + day = journal / "chronicle" / "20240101" 189 + day.mkdir(parents=True) 190 190 191 191 # Create an audio jsonl file in segment directory 192 192 ts_dir = day / "default" / "123456_300" ··· 227 227 """Modifying a facet event file invalidates that day's cache.""" 228 228 stats_mod = importlib.import_module("think.journal_stats") 229 229 journal = tmp_path 230 - day = journal / "20240101" 231 - day.mkdir() 230 + day = journal / "chronicle" / "20240101" 231 + day.mkdir(parents=True) 232 232 233 233 # Create minimal day content 234 234 ts_dir = day / "default" / "123456_300" ··· 287 287 """Test that the new unified token format is properly handled.""" 288 288 stats_mod = importlib.import_module("think.journal_stats") 289 289 journal = tmp_path 290 - day1 = journal / "20240101" 291 - day1.mkdir() 290 + day1 = journal / "chronicle" / "20240101" 291 + day1.mkdir(parents=True) 292 292 293 293 # Create tokens directory with new format token files 294 294 tokens_dir = journal / "tokens" ··· 336 336 """Int-valued fields in usage are all counted; top-level metadata is ignored.""" 337 337 stats_mod = importlib.import_module("think.journal_stats") 338 338 journal = tmp_path 339 - day1 = journal / "20240101" 340 - day1.mkdir() 339 + day1 = journal / "chronicle" / "20240101" 340 + day1.mkdir(parents=True) 341 341 342 342 tokens_dir = journal / "tokens" 343 343 tokens_dir.mkdir()
-1
tests/test_matching.py
··· 4 4 """Tests for entity matching and name variant resolution.""" 5 5 6 6 from think.entities.matching import ( 7 - MatchResult, 8 7 MatchTier, 9 8 build_name_resolution_map, 10 9 find_matching_entity,
+2 -2
tests/test_output_hooks.py
··· 15 15 import os 16 16 from pathlib import Path 17 17 18 + from tests.conftest import copytree_tracked 18 19 from think.agents import _apply_template_vars 19 20 from think.talent import load_post_hook, load_pre_hook 20 21 from think.utils import day_path 21 - from tests.conftest import copytree_tracked 22 22 23 23 FIXTURES = Path("tests/fixtures") 24 24 ··· 26 26 def copy_day(tmp_path: Path) -> Path: 27 27 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 28 28 dest = day_path("20240101") 29 - src = FIXTURES / "journal" / "20240101" 29 + src = FIXTURES / "journal" / "chronicle" / "20240101" 30 30 copytree_tracked(src, dest) 31 31 return dest 32 32
+16 -10
tests/test_pipeline_health.py
··· 53 53 54 54 55 55 def test_missing_health_dir(pipeline_journal): 56 - (pipeline_journal / "20260101").mkdir() 56 + (pipeline_journal / "chronicle" / "20260101").mkdir(parents=True) 57 57 58 58 summary = summarize_pipeline_day("20260101") 59 59 ··· 64 64 65 65 def test_healthy_day_with_all_modes(pipeline_journal): 66 66 day = "20990101" 67 - base = pipeline_journal / day / "health" 67 + base = pipeline_journal / "chronicle" / day / "health" 68 68 _write_jsonl( 69 69 base / "1_segment_dream.jsonl", 70 70 [ ··· 107 107 def test_agent_failure_promotes_warning(pipeline_journal): 108 108 day = "20990102" 109 109 _write_jsonl( 110 - pipeline_journal / day / "health" / "1_segment_dream.jsonl", 110 + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 111 111 [ 112 112 { 113 113 "event": "agent.fail", ··· 149 149 } 150 150 for idx in range(25) 151 151 ] 152 - _write_jsonl(pipeline_journal / day / "health" / "1_daily_dream.jsonl", events) 152 + _write_jsonl( 153 + pipeline_journal / "chronicle" / day / "health" / "1_daily_dream.jsonl", events 154 + ) 153 155 154 156 summary = summarize_pipeline_day(day) 155 157 ··· 162 164 def test_activity_detected_without_run_is_stale(pipeline_journal): 163 165 day = "20990104" 164 166 _write_jsonl( 165 - pipeline_journal / day / "health" / "1_segment_dream.jsonl", 167 + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 166 168 [{"event": "activity.detected", "mode": "segment"}], 167 169 ) 168 170 ··· 175 177 def test_past_day_without_daily_run_is_stale(pipeline_journal, monkeypatch): 176 178 day = "20200101" 177 179 _write_jsonl( 178 - pipeline_journal / day / "health" / "1_segment_dream.jsonl", 180 + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 179 181 [{"event": "run.start", "mode": "segment"}], 180 182 ) 181 183 monkeypatch.setattr( ··· 191 193 def test_today_before_23h_no_daily_run_is_healthy(pipeline_journal, monkeypatch): 192 194 current = datetime(2026, 4, 16, 12, 0, 0) 193 195 monkeypatch.setattr("think.pipeline_health._now", lambda: current) 194 - (pipeline_journal / current.strftime("%Y%m%d") / "health").mkdir(parents=True) 196 + (pipeline_journal / "chronicle" / current.strftime("%Y%m%d") / "health").mkdir( 197 + parents=True 198 + ) 195 199 196 200 summary = summarize_pipeline_day(current.strftime("%Y%m%d")) 197 201 ··· 202 206 def test_today_after_23h_no_daily_run_is_stale(pipeline_journal, monkeypatch): 203 207 current = datetime(2026, 4, 16, 23, 30, 0) 204 208 monkeypatch.setattr("think.pipeline_health._now", lambda: current) 205 - (pipeline_journal / current.strftime("%Y%m%d") / "health").mkdir(parents=True) 209 + (pipeline_journal / "chronicle" / current.strftime("%Y%m%d") / "health").mkdir( 210 + parents=True 211 + ) 206 212 207 213 summary = summarize_pipeline_day(current.strftime("%Y%m%d")) 208 214 ··· 212 218 213 219 def test_segment_runs_missing_is_soft(pipeline_journal, monkeypatch): 214 220 day = "20990105" 215 - (pipeline_journal / day / "health").mkdir(parents=True) 221 + (pipeline_journal / "chronicle" / day / "health").mkdir(parents=True) 216 222 monkeypatch.setattr( 217 223 "think.pipeline_health.iter_segments", 218 224 lambda value: [("default", "120000_300", Path("/tmp/fake"))], ··· 241 247 242 248 def test_malformed_json_lines_skipped(pipeline_journal): 243 249 day = "20990106" 244 - path = pipeline_journal / day / "health" / "1_segment_dream.jsonl" 250 + path = pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl" 245 251 path.parent.mkdir(parents=True, exist_ok=True) 246 252 path.write_text( 247 253 json.dumps({"event": "run.start", "mode": "segment"})
+23 -5
tests/test_pipeline_smoke.py
··· 141 141 ) 142 142 143 143 for segment_key, sense_dict in SEGMENTS: 144 - seg_dir = journal / DAY / STREAM / segment_key 144 + seg_dir = journal / "chronicle" / DAY / STREAM / segment_key 145 145 (seg_dir / "agents").mkdir(parents=True, exist_ok=True) 146 146 (seg_dir / "agents" / "sense.json").write_text(json.dumps(sense_dict)) 147 147 ··· 161 161 "091500_300", 162 162 "100000_300", 163 163 ]: 164 - seg_agents = journal / DAY / STREAM / seg_key / "agents" 164 + seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "agents" 165 165 assert (seg_agents / "sense.json").exists() 166 166 assert (seg_agents / "activity.md").exists() 167 167 assert (seg_agents / "density.json").exists() ··· 174 174 for seg_key in ["091000_300", "091500_300"]: 175 175 speakers = json.loads( 176 176 ( 177 - journal / DAY / STREAM / seg_key / "agents" / "speakers.json" 177 + journal 178 + / "chronicle" 179 + / DAY 180 + / STREAM 181 + / seg_key 182 + / "agents" 183 + / "speakers.json" 178 184 ).read_text() 179 185 ) 180 186 assert speakers == ["Alice", "Bob"] 181 187 182 188 for seg_key in ["090000_300", "090500_300", "100000_300"]: 183 189 assert not ( 184 - journal / DAY / STREAM / seg_key / "agents" / "speakers.json" 190 + journal 191 + / "chronicle" 192 + / DAY 193 + / STREAM 194 + / seg_key 195 + / "agents" 196 + / "speakers.json" 185 197 ).exists() 186 198 187 199 idle_density = json.loads( 188 200 ( 189 - journal / DAY / STREAM / "092000_300" / "agents" / "density.json" 201 + journal 202 + / "chronicle" 203 + / DAY 204 + / STREAM 205 + / "092000_300" 206 + / "agents" 207 + / "density.json" 190 208 ).read_text() 191 209 ) 192 210 assert idle_density["classification"] == "idle"
+19 -10
tests/test_retention.py
··· 281 281 journal = tmp_path / "journal" 282 282 283 283 # Day 1: 60 days old — two complete segments 284 - day1 = journal / "20260115" / "default" / "100000_300" 284 + day1 = journal / "chronicle" / "20260115" / "default" / "100000_300" 285 285 day1.mkdir(parents=True) 286 286 (day1 / "audio.flac").write_bytes(b"x" * 1000) 287 287 (day1 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') 288 288 (day1 / "stream.json").write_text('{"stream":"default"}') 289 289 (day1 / "agents").mkdir() 290 290 291 - day1b = journal / "20260115" / "plaud" / "103000_300" 291 + day1b = journal / "chronicle" / "20260115" / "plaud" / "103000_300" 292 292 day1b.mkdir(parents=True) 293 293 (day1b / "audio.m4a").write_bytes(b"x" * 500) 294 294 (day1b / "audio.jsonl").write_text('{"raw":"audio.m4a"}\n') ··· 296 296 (day1b / "agents").mkdir() 297 297 298 298 # Day 2: recent — one complete segment (must stay within 30d window) 299 - day2 = journal / "20260401" / "default" / "120000_300" 299 + day2 = journal / "chronicle" / "20260401" / "default" / "120000_300" 300 300 day2.mkdir(parents=True) 301 301 (day2 / "audio.flac").write_bytes(b"x" * 800) 302 302 (day2 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') ··· 304 304 (day2 / "agents").mkdir() 305 305 306 306 # Day 3: incomplete segment (no audio.jsonl) 307 - day3 = journal / "20260101" / "default" / "140000_300" 307 + day3 = journal / "chronicle" / "20260101" / "default" / "140000_300" 308 308 day3.mkdir(parents=True) 309 309 (day3 / "audio.flac").write_bytes(b"x" * 600) 310 310 (day3 / "stream.json").write_text('{"stream":"default"}') ··· 325 325 # Should report but not delete 326 326 assert result.files_deleted == 2 # day1 default + plaud 327 327 assert result.bytes_freed == 1500 328 - assert (journal / "20260115" / "default" / "100000_300" / "audio.flac").exists() 329 - assert (journal / "20260115" / "plaud" / "103000_300" / "audio.m4a").exists() 328 + assert ( 329 + journal / "chronicle" / "20260115" / "default" / "100000_300" / "audio.flac" 330 + ).exists() 331 + assert ( 332 + journal / "chronicle" / "20260115" / "plaud" / "103000_300" / "audio.m4a" 333 + ).exists() 330 334 # No retention log for dry run 331 335 assert not (journal / "health" / "retention.log").exists() 332 336 ··· 338 342 assert result.files_deleted == 2 339 343 # Files should be gone 340 344 assert not ( 341 - journal / "20260115" / "default" / "100000_300" / "audio.flac" 345 + journal / "chronicle" / "20260115" / "default" / "100000_300" / "audio.flac" 342 346 ).exists() 343 347 assert not ( 344 - journal / "20260115" / "plaud" / "103000_300" / "audio.m4a" 348 + journal / "chronicle" / "20260115" / "plaud" / "103000_300" / "audio.m4a" 345 349 ).exists() 346 350 # Derived content preserved 347 351 assert ( 348 - journal / "20260115" / "default" / "100000_300" / "audio.jsonl" 352 + journal 353 + / "chronicle" 354 + / "20260115" 355 + / "default" 356 + / "100000_300" 357 + / "audio.jsonl" 349 358 ).exists() 350 359 # Retention log written 351 360 assert (journal / "health" / "retention.log").exists() ··· 437 446 438 447 def test_processed_at_reflects_latest_mtime(self, tmp_path, monkeypatch): 439 448 journal = self._setup_journal(tmp_path, monkeypatch) 440 - segment = journal / "20260115" / "default" / "100000_300" 449 + segment = journal / "chronicle" / "20260115" / "default" / "100000_300" 441 450 audio_jsonl = segment / "audio.jsonl" 442 451 alternate_audio_jsonl = segment / "meeting_audio.jsonl" 443 452 speaker_labels = segment / "agents" / "speaker_labels.json"
+4 -4
tests/test_runner.py
··· 266 266 from datetime import datetime 267 267 268 268 day = datetime.now().strftime("%Y%m%d") 269 - log_path = journal_path / day / "health" / f"{ref}_echo.log" 269 + log_path = journal_path / "chronicle" / day / "health" / f"{ref}_echo.log" 270 270 271 271 assert log_path.exists() 272 272 content = log_path.read_text() 273 273 assert "logged output" in content 274 274 275 275 # Verify day-level symlink exists 276 - day_symlink = journal_path / day / "health" / "echo.log" 276 + day_symlink = journal_path / "chronicle" / day / "health" / "echo.log" 277 277 assert day_symlink.is_symlink() 278 278 assert day_symlink.resolve() == log_path.resolve() 279 279 ··· 292 292 managed.cleanup() 293 293 294 294 # Log should be in target day, not today 295 - log_path = journal_path / target_day / "health" / f"{ref}_echo.log" 295 + log_path = journal_path / "chronicle" / target_day / "health" / f"{ref}_echo.log" 296 296 assert log_path.exists() 297 297 content = log_path.read_text() 298 298 assert "day test" in content ··· 306 306 assert not today_log.exists() 307 307 308 308 # Day-level symlink in target day 309 - day_symlink = journal_path / target_day / "health" / "echo.log" 309 + day_symlink = journal_path / "chronicle" / target_day / "health" / "echo.log" 310 310 assert day_symlink.is_symlink() 311 311 assert day_symlink.resolve() == log_path.resolve() 312 312
+15 -13
tests/test_segment.py
··· 23 23 agents=None, 24 24 ): 25 25 """Create a minimal segment fixture directory.""" 26 - seg_dir = base / day / stream / segment 26 + seg_dir = base / "chronicle" / day / stream / segment 27 27 seg_dir.mkdir(parents=True) 28 28 if stream_json is not None: 29 29 (seg_dir / "stream.json").write_text(json.dumps(stream_json)) ··· 560 560 ) 561 561 cmd_move(args) 562 562 563 - assert not (tmp_path / "20240101" / "default" / "090000_300").exists() 564 - assert (tmp_path / "20240115" / "default" / "090000_300").is_dir() 563 + assert not (tmp_path / "chronicle" / "20240101" / "default" / "090000_300").exists() 564 + assert (tmp_path / "chronicle" / "20240115" / "default" / "090000_300").is_dir() 565 565 import think.streams 566 566 567 567 marker = think.streams.read_segment_stream( 568 - tmp_path / "20240115" / "default" / "090000_300" 568 + tmp_path / "chronicle" / "20240115" / "default" / "090000_300" 569 569 ) 570 570 assert marker["stream"] == "default" 571 571 assert marker["seq"] == 1 ··· 601 601 ) 602 602 cmd_move(args) 603 603 604 - assert not (tmp_path / "20240101" / "default" / "090000_300").exists() 605 - assert (tmp_path / "20240101" / "default" / "140000_300").is_dir() 604 + assert not (tmp_path / "chronicle" / "20240101" / "default" / "090000_300").exists() 605 + assert (tmp_path / "chronicle" / "20240101" / "default" / "140000_300").is_dir() 606 606 607 607 608 608 def test_move_dry_run(tmp_path, monkeypatch, capsys): ··· 638 638 639 639 assert "[dry run]" in out 640 640 assert "090000_300" in out 641 - assert (tmp_path / "20240101" / "default" / "090000_300").is_dir() 641 + assert (tmp_path / "chronicle" / "20240101" / "default" / "090000_300").is_dir() 642 642 643 643 644 644 def test_move_collision_no_to_time(tmp_path, monkeypatch, capsys): ··· 722 722 cmd_move(args) 723 723 out = capsys.readouterr().out 724 724 725 - assert (tmp_path / "20240115" / "default" / "090000_300").is_dir() 725 + assert (tmp_path / "chronicle" / "20240115" / "default" / "090000_300").is_dir() 726 726 assert "events.jsonl lines: 0" in out 727 727 728 728 ··· 771 771 import think.streams 772 772 773 773 succ_marker = think.streams.read_segment_stream( 774 - tmp_path / "20240101" / "default" / "140000_300" 774 + tmp_path / "chronicle" / "20240101" / "default" / "140000_300" 775 775 ) 776 776 assert succ_marker["prev_day"] == "20240115" 777 777 assert succ_marker["prev_segment"] == "090000_300" ··· 821 821 out = capsys.readouterr().out 822 822 823 823 assert "successor to patch: (none - stream tail)" in out 824 - assert (tmp_path / "20240115" / "default" / "140000_300").is_dir() 824 + assert (tmp_path / "chronicle" / "20240115" / "default" / "140000_300").is_dir() 825 825 826 826 827 827 def test_move_rewrites_events_jsonl(tmp_path, monkeypatch, capsys): ··· 868 868 ) 869 869 cmd_move(args) 870 870 871 - new_events_path = tmp_path / "20240115" / "default" / "140000_300" / "events.jsonl" 871 + new_events_path = ( 872 + tmp_path / "chronicle" / "20240115" / "default" / "140000_300" / "events.jsonl" 873 + ) 872 874 assert new_events_path.exists() 873 875 lines = [ 874 876 json.loads(line) ··· 909 911 ) 910 912 cmd_move(args) 911 913 912 - assert (tmp_path / "20240101" / "health" / "stream.updated").exists() 913 - assert (tmp_path / "20240115" / "health" / "stream.updated").exists() 914 + assert (tmp_path / "chronicle" / "20240101" / "health" / "stream.updated").exists() 915 + assert (tmp_path / "chronicle" / "20240115" / "health" / "stream.updated").exists() 914 916 915 917 916 918 def test_move_same_location_refused(tmp_path, monkeypatch, capsys):
+13 -7
tests/test_segment_ingest.py
··· 28 28 def journal_env(tmp_path, monkeypatch): 29 29 """Set up journal root and source storage.""" 30 30 monkeypatch.setattr(convey.state, "journal_root", str(tmp_path), raising=False) 31 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 31 32 (tmp_path / "apps" / "import" / "journal_sources").mkdir( 32 33 parents=True, exist_ok=True 33 34 ) ··· 156 157 "errors": [], 157 158 } 158 159 159 - first_dir = env["root"] / "20260413" / "laptop" / "143022_300" 160 - second_dir = env["root"] / "20260413" / "laptop" / "143500_300" 160 + first_dir = env["root"] / "chronicle" / "20260413" / "laptop" / "143022_300" 161 + second_dir = env["root"] / "chronicle" / "20260413" / "laptop" / "143500_300" 161 162 assert (first_dir / "audio.flac").read_bytes() == b"audio one" 162 163 assert (first_dir / "transcript.jsonl").read_bytes() == b'{"text":"one"}\n' 163 164 assert (second_dir / "audio.flac").read_bytes() == b"audio two" ··· 224 225 225 226 def test_ingest_deconfliction(ingest_env, monkeypatch): 226 227 env = ingest_env 227 - target_dir = env["root"] / "20260413" / "laptop" / "143022_300" 228 + target_dir = env["root"] / "chronicle" / "20260413" / "laptop" / "143022_300" 228 229 target_dir.mkdir(parents=True, exist_ok=True) 229 230 (target_dir / "audio.flac").write_bytes(b"existing audio") 230 231 ··· 251 252 "errors": [], 252 253 } 253 254 assert ( 254 - env["root"] / "20260413" / "laptop" / "143023_300" / "audio.flac" 255 + env["root"] / "chronicle" / "20260413" / "laptop" / "143023_300" / "audio.flac" 255 256 ).read_bytes() == b"new audio" 256 257 257 258 state_data = _read_state(env["key_prefix"]) ··· 296 297 } 297 298 ] 298 299 assert ( 299 - env["root"] / "20260413" / "laptop" / "143500_300" / "audio.flac" 300 + env["root"] / "chronicle" / "20260413" / "laptop" / "143500_300" / "audio.flac" 300 301 ).read_bytes() == b"good" 301 302 302 303 ··· 435 436 436 437 def test_ingest_skip_ignores_extra_existing_files(ingest_env): 437 438 env = ingest_env 438 - segment_dir = env["root"] / "20260413" / "laptop" / "143022_300" 439 + segment_dir = env["root"] / "chronicle" / "20260413" / "laptop" / "143022_300" 439 440 segment_dir.mkdir(parents=True, exist_ok=True) 440 441 (segment_dir / "audio.flac").write_bytes(b"audio one") 441 442 (segment_dir / "extra.txt").write_bytes(b"keep me") ··· 550 551 state_data = _read_state(env["key_prefix"]) 551 552 assert "_default/143022_300" in state_data["20260413"] 552 553 assert ( 553 - env["root"] / "20260413" / "_default" / "143022_300" / "transcript.jsonl" 554 + env["root"] 555 + / "chronicle" 556 + / "20260413" 557 + / "_default" 558 + / "143022_300" 559 + / "transcript.jsonl" 554 560 ).read_bytes() == b'{"text":"default"}\n' 555 561 556 562
+18 -18
tests/test_sense.py
··· 164 164 writer.close() 165 165 166 166 # Log file uses {ref}_{name}.log format 167 - log_path = tmp_path / "20241101" / "health" / f"{ref}_test.log" 167 + log_path = tmp_path / "chronicle" / "20241101" / "health" / f"{ref}_test.log" 168 168 assert log_path.exists() 169 169 content = log_path.read_text() 170 170 assert "line 1\n" in content 171 171 assert "line 2\n" in content 172 172 173 173 # Verify symlinks exist 174 - day_symlink = tmp_path / "20241101" / "health" / "test.log" 174 + day_symlink = tmp_path / "chronicle" / "20241101" / "health" / "test.log" 175 175 assert day_symlink.is_symlink() 176 176 journal_symlink = tmp_path / "health" / "test.log" 177 177 assert journal_symlink.is_symlink() ··· 204 204 writer.close() 205 205 206 206 # Log file uses {ref}_{name}.log format 207 - log_path = tmp_path / "20241101" / "health" / f"{ref}_test.log" 207 + log_path = tmp_path / "chronicle" / "20241101" / "health" / f"{ref}_test.log" 208 208 lines = log_path.read_text().split("\n") 209 209 # Should have 50 lines (5 threads * 10 lines each) 210 210 assert len([line for line in lines if line]) == 50 ··· 245 245 with tempfile.TemporaryDirectory() as tmpdir: 246 246 # Create journal/day/stream/segment structure 247 247 journal_dir = Path(tmpdir) 248 - day_dir = journal_dir / "20250101" 248 + day_dir = journal_dir / "chronicle" / "20250101" 249 249 segment_dir = day_dir / "default" / "123456_300" 250 250 segment_dir.mkdir(parents=True) 251 251 ··· 286 286 287 287 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 288 288 289 - day_dir = tmp_path / "20250101" 289 + day_dir = tmp_path / "chronicle" / "20250101" 290 290 segment_dir = day_dir / "default" / "143022_300" 291 291 segment_dir.mkdir(parents=True) 292 292 ··· 316 316 317 317 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 318 318 319 - day_dir = tmp_path / "20250101" 319 + day_dir = tmp_path / "chronicle" / "20250101" 320 320 segment_1 = day_dir / "default" / "143022_300" 321 321 segment_2 = day_dir / "default" / "150022_300" 322 322 segment_1.mkdir(parents=True) ··· 368 368 assert args == ["echo", str(test_file)] 369 369 370 370 # Verify log file was created with {ref}_echo.log format 371 - health_dir = tmp_path / "20241101" / "health" 371 + health_dir = tmp_path / "chronicle" / "20241101" / "health" 372 372 log_files = list(health_dir.glob("*_echo.log")) 373 373 assert len(log_files) == 1, f"Expected 1 echo log file, found {len(log_files)}" 374 374 ··· 376 376 def test_file_sensor_spawn_handler_duplicate(tmp_path, mock_callosum): 377 377 """Test that duplicate file processing is prevented.""" 378 378 # Create journal/day structure 379 - day_dir = tmp_path / "20250101" 380 - day_dir.mkdir() 379 + day_dir = tmp_path / "chronicle" / "20250101" 380 + day_dir.mkdir(parents=True) 381 381 382 382 sensor = FileSensor(tmp_path) 383 383 sensor.register("*.txt", "test", ["echo", "hello"]) ··· 424 424 assert test_file not in sensor.running 425 425 426 426 # Check log file contains output with {ref}_echo.log format 427 - health_dir = tmp_path / "20241101" / "health" 427 + health_dir = tmp_path / "chronicle" / "20241101" / "health" 428 428 log_files = list(health_dir.glob("*_echo.log")) 429 429 assert len(log_files) == 1, f"Expected 1 echo log file, found {len(log_files)}" 430 430 ··· 486 486 """Test file handling dispatches to correct handler.""" 487 487 with patch.object(FileSensor, "_spawn_handler") as mock_spawn: 488 488 # Create journal/day/stream/segment structure 489 - day_dir = tmp_path / "20250101" 489 + day_dir = tmp_path / "chronicle" / "20250101" 490 490 segment_dir = day_dir / "default" / "143022_300" 491 491 segment_dir.mkdir(parents=True) 492 492 ··· 536 536 """Test handling of observe.observing Callosum events.""" 537 537 with patch.object(FileSensor, "_handle_file") as mock_handle: 538 538 # Create journal/day/stream/segment structure 539 - day_dir = tmp_path / "20250101" 539 + day_dir = tmp_path / "chronicle" / "20250101" 540 540 segment_dir = day_dir / "default" / "143022_300" 541 541 segment_dir.mkdir(parents=True) 542 542 ··· 618 618 from think.callosum import CallosumConnection 619 619 620 620 # Create journal/day/stream/segment structure 621 - day_dir = tmp_path / "20250101" 621 + day_dir = tmp_path / "chronicle" / "20250101" 622 622 segment_dir = day_dir / "default" / "143022_300" 623 623 segment_dir.mkdir(parents=True) 624 624 ··· 671 671 from think.callosum import CallosumConnection 672 672 673 673 # Create journal/day/stream/segment structure 674 - day_dir = tmp_path / "20250101" 674 + day_dir = tmp_path / "chronicle" / "20250101" 675 675 segment_dir = day_dir / "default" / "143022_300" 676 676 segment_dir.mkdir(parents=True) 677 677 ··· 720 720 from observe.sense import delete_outputs 721 721 722 722 # Create journal/day/stream/segment structure 723 - day_dir = tmp_path / "20250101" 723 + day_dir = tmp_path / "chronicle" / "20250101" 724 724 segment_dir = day_dir / "default" / "143022_300" 725 725 segment_dir.mkdir(parents=True) 726 726 ··· 744 744 from observe.sense import delete_outputs 745 745 746 746 # Create journal/day/stream/segment structure 747 - day_dir = tmp_path / "20250101" 747 + day_dir = tmp_path / "chronicle" / "20250101" 748 748 segment_dir = day_dir / "default" / "143022_300" 749 749 segment_dir.mkdir(parents=True) 750 750 ··· 768 768 from observe.sense import delete_outputs 769 769 770 770 # Create journal/day/stream/segment structure 771 - day_dir = tmp_path / "20250101" 771 + day_dir = tmp_path / "chronicle" / "20250101" 772 772 segment_dir = day_dir / "default" / "143022_300" 773 773 segment_dir.mkdir(parents=True) 774 774 ··· 788 788 from observe.sense import delete_outputs 789 789 790 790 # Create journal/day/stream/segments structure 791 - day_dir = tmp_path / "20250101" 791 + day_dir = tmp_path / "chronicle" / "20250101" 792 792 segment1 = day_dir / "default" / "143022_300" 793 793 segment2 = day_dir / "default" / "150022_300" 794 794 segment1.mkdir(parents=True)
+5 -1
tests/test_sol.py
··· 228 228 # root should NOT end with /journal — that's --path 229 229 assert not path.endswith("/journal") 230 230 # should be a parent of the journal path 231 - assert path.endswith("/solstone") or "/solstone" in path 231 + assert ( 232 + path.endswith("/solstone") 233 + or "/solstone" in path 234 + or path.endswith("/worktree") 235 + ) 232 236 233 237 def test_main_unknown_command_exits(self, monkeypatch): 234 238 """Test that unknown command exits with code 1."""
+1 -2
tests/test_stats_contract.py
··· 5 5 import json 6 6 from pathlib import Path 7 7 8 - 9 8 CONTRACT_FIELDS = [ 10 9 ("days", "stats.days"), 11 10 ("totals", "stats.totals"), ··· 57 56 58 57 def _build_journal(base_path): 59 58 journal = base_path 60 - day = journal / "20240101" 59 + day = journal / "chronicle" / "20240101" 61 60 seg1 = day / "default" / "123456_300" 62 61 seg2 = day / "default" / "134500_300" 63 62 seg1.mkdir(parents=True)
+4 -4
tests/test_stats_schema.py
··· 11 11 stats_mod = importlib.import_module("think.journal_stats") 12 12 schema_mod = importlib.import_module("think.stats_schema") 13 13 journal = tmp_path 14 - day = journal / "20240101" 15 - day.mkdir() 14 + day = journal / "chronicle" / "20240101" 15 + day.mkdir(parents=True) 16 16 17 17 # Create minimal transcript fixture 18 18 ts_dir = day / "default" / "123456_300" ··· 79 79 stats_mod = importlib.import_module("think.journal_stats") 80 80 schema_mod = importlib.import_module("think.stats_schema") 81 81 journal = tmp_path 82 - day = journal / "20240101" 83 - day.mkdir() 82 + day = journal / "chronicle" / "20240101" 83 + day.mkdir(parents=True) 84 84 85 85 # Create transcript and percept fixtures 86 86 ts_dir = day / "default" / "123456_300"
+5 -5
tests/test_streams.py
··· 20 20 # --- stream_name tests --- 21 21 22 22 23 - def test_stream_name_observer(): 23 + def test_stream_name_host(): 24 24 """Host only -> hostname.""" 25 25 assert stream_name(host="archon") == "archon" 26 26 ··· 141 141 142 142 def test_write_read_segment_stream(tmp_path): 143 143 """Round-trip write/read stream.json.""" 144 - seg_dir = tmp_path / "20250119" / "default" / "142500_300" 144 + seg_dir = tmp_path / "chronicle" / "20250119" / "default" / "142500_300" 145 145 seg_dir.mkdir(parents=True) 146 146 147 147 write_segment_stream(seg_dir, "archon", "20250119", "142000_300", 5) ··· 156 156 157 157 def test_write_segment_stream_first(tmp_path): 158 158 """First segment has None prev values.""" 159 - seg_dir = tmp_path / "20250119" / "default" / "142500_300" 159 + seg_dir = tmp_path / "chronicle" / "20250119" / "default" / "142500_300" 160 160 seg_dir.mkdir(parents=True) 161 161 162 162 write_segment_stream(seg_dir, "archon", None, None, 1) ··· 169 169 170 170 def test_read_segment_stream_missing(tmp_path): 171 171 """Returns None for pre-stream segments.""" 172 - seg_dir = tmp_path / "20250119" / "default" / "142500_300" 172 + seg_dir = tmp_path / "chronicle" / "20250119" / "default" / "142500_300" 173 173 seg_dir.mkdir(parents=True) 174 174 175 175 assert read_segment_stream(seg_dir) is None ··· 202 202 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 203 203 204 204 # Create segment dirs with stream markers under default stream 205 - day_dir = tmp_path / "20250119" 205 + day_dir = tmp_path / "chronicle" / "20250119" 206 206 seg1 = day_dir / "default" / "142500_300" 207 207 seg2 = day_dir / "default" / "143000_300" 208 208 seg1.mkdir(parents=True)
-2
tests/test_supervisor.py
··· 132 132 133 133 def test_shutdown_stops_in_reverse_order(monkeypatch): 134 134 """Shutdown stops services in reverse order.""" 135 - mod = importlib.import_module("think.supervisor") 136 - 137 135 operations = [] 138 136 139 137 class MockProc:
+37
tests/test_template_substitution.py
··· 251 251 result_empty = load_prompt("plain", base_dir=mock_prompt_dir, context={}) 252 252 253 253 assert result_none.text == result_empty.text 254 + 255 + 256 + def test_load_prompt_sol_vars_follow_journal_override(monkeypatch, tmp_path): 257 + """Journal sol/ content should not leak across journal overrides.""" 258 + 259 + def write_journal(journal_dir, awareness_text): 260 + config_dir = journal_dir / "config" 261 + config_dir.mkdir(parents=True) 262 + (config_dir / "journal.json").write_text( 263 + json.dumps( 264 + { 265 + "identity": {"name": "Test User"}, 266 + "agent": {"name": "sol", "name_status": "default"}, 267 + } 268 + ) 269 + ) 270 + sol_dir = journal_dir / "sol" 271 + sol_dir.mkdir() 272 + (sol_dir / "awareness.md").write_text(awareness_text) 273 + 274 + prompt_dir = tmp_path / "prompts" 275 + prompt_dir.mkdir() 276 + (prompt_dir / "sol_vars.md").write_text("Awareness:\n$sol_awareness\n") 277 + 278 + journal_one = tmp_path / "journal-one" 279 + journal_two = tmp_path / "journal-two" 280 + write_journal(journal_one, "first awareness") 281 + write_journal(journal_two, "second awareness") 282 + 283 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_one)) 284 + first = load_prompt("sol_vars", base_dir=prompt_dir) 285 + assert "first awareness" in first.text 286 + 287 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_two)) 288 + second = load_prompt("sol_vars", base_dir=prompt_dir) 289 + assert "second awareness" in second.text 290 + assert "first awareness" not in second.text
+7 -7
tests/test_think_utils.py
··· 18 18 DEFAULT_STREAM, 19 19 day_from_path, 20 20 iter_segments, 21 - segment_parse, 22 21 segment_key, 22 + segment_parse, 23 23 setup_cli, 24 24 ) 25 25 ··· 830 830 class TestIterSegments: 831 831 def test_skips_health_directory(self, tmp_path): 832 832 """iter_segments does not return segments from health/ dirs.""" 833 - day_dir = tmp_path / "20240101" 834 - day_dir.mkdir() 833 + day_dir = tmp_path / "chronicle" / "20240101" 834 + day_dir.mkdir(parents=True) 835 835 health_seg = day_dir / "health" / "120000_300" 836 836 health_seg.mkdir(parents=True) 837 837 normal_seg = day_dir / "default" / "130000_300" ··· 844 844 845 845 def test_toplevel_segments_as_default_stream(self, tmp_path): 846 846 """Top-level segment dirs are returned with _default stream name.""" 847 - day_dir = tmp_path / "20240101" 848 - day_dir.mkdir() 847 + day_dir = tmp_path / "chronicle" / "20240101" 848 + day_dir.mkdir(parents=True) 849 849 toplevel_seg = day_dir / "143022_300" 850 850 toplevel_seg.mkdir() 851 851 normal_seg = day_dir / "default" / "150000_300" ··· 861 861 862 862 def test_normal_stream_discovery_unchanged(self, tmp_path): 863 863 """Normal stream/segment discovery still works correctly.""" 864 - day_dir = tmp_path / "20240101" 865 - day_dir.mkdir() 864 + day_dir = tmp_path / "chronicle" / "20240101" 865 + day_dir.mkdir(parents=True) 866 866 (day_dir / "default" / "100000_300").mkdir(parents=True) 867 867 (day_dir / "default" / "110000_300").mkdir(parents=True) 868 868 (day_dir / "import.apple" / "120000_600").mkdir(parents=True)
+4 -4
tests/test_transcribe_cli.py
··· 12 12 13 13 def test_main_accepts_journal_relative_path(tmp_path, monkeypatch): 14 14 """main() resolves audio_path relative to journal when absolute path fails.""" 15 - seg_dir = tmp_path / "20260201" / "default" / "090000_300" 15 + seg_dir = tmp_path / "chronicle" / "20260201" / "default" / "090000_300" 16 16 seg_dir.mkdir(parents=True) 17 17 audio_file = seg_dir / "audio.wav" 18 18 audio_file.touch() ··· 78 78 79 79 def _make_batch_journal(tmp_path: Path) -> Path: 80 80 """Create a minimal temp journal with three segments for batch testing.""" 81 - seg1 = tmp_path / "20260101" / "default" / "090000_300" 81 + seg1 = tmp_path / "chronicle" / "20260101" / "default" / "090000_300" 82 82 seg1.mkdir(parents=True) 83 83 (seg1 / "audio.flac").touch() 84 84 85 - seg2 = tmp_path / "20260101" / "default" / "140000_300" 85 + seg2 = tmp_path / "chronicle" / "20260101" / "default" / "140000_300" 86 86 seg2.mkdir(parents=True) 87 87 (seg2 / "audio.flac").touch() 88 88 (seg2 / "audio.jsonl").touch() 89 89 90 - seg3 = tmp_path / "20260101" / "default" / "180000_300" 90 + seg3 = tmp_path / "chronicle" / "20260101" / "default" / "180000_300" 91 91 seg3.mkdir(parents=True) 92 92 (seg3 / "screen.png").touch() 93 93
+12 -12
tests/test_transfer.py
··· 90 90 91 91 # Set up mock journal with day/stream/segment structure 92 92 journal_path = tmp_path / "journal" 93 - day_dir = journal_path / "20250101" 93 + day_dir = journal_path / "chronicle" / "20250101" 94 94 segment_dir = day_dir / "default" / "120000_300" 95 95 segment_dir.mkdir(parents=True) 96 96 ··· 130 130 from observe.transfer import create_archive 131 131 132 132 journal_path = tmp_path / "journal" 133 - day_dir = journal_path / "20250101" 133 + day_dir = journal_path / "chronicle" / "20250101" 134 134 day_dir.mkdir(parents=True) 135 135 136 136 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) ··· 249 249 250 250 # Set up journal with matching segment 251 251 journal_path = tmp_path / "journal" 252 - segment_dir = journal_path / "20250101" / "120000_300" 252 + segment_dir = journal_path / "chronicle" / "20250101" / "120000_300" 253 253 segment_dir.mkdir(parents=True) 254 254 (segment_dir / "audio.flac").write_bytes(content) 255 255 ··· 276 276 277 277 # Set up journal with different content in same segment 278 278 journal_path = tmp_path / "journal" 279 - segment_dir = journal_path / "20250101" / "120000_300" 279 + segment_dir = journal_path / "chronicle" / "20250101" / "120000_300" 280 280 segment_dir.mkdir(parents=True) 281 281 (segment_dir / "audio.flac").write_bytes(b"existing different data") 282 282 ··· 327 327 assert "120000_300" in result["imported"] 328 328 329 329 # Verify files were extracted 330 - segment_dir = journal_path / "20250101" / "120000_300" 330 + segment_dir = journal_path / "chronicle" / "20250101" / "120000_300" 331 331 assert segment_dir.exists() 332 332 assert (segment_dir / "audio.flac").read_bytes() == audio_content 333 333 assert (segment_dir / "audio.jsonl").read_bytes() == jsonl_content ··· 354 354 355 355 assert result["status"] == "dry_run" 356 356 # Directory should not be created 357 - assert not (journal_path / "20250101").exists() 357 + assert not (journal_path / "chronicle" / "20250101").exists() 358 358 359 359 def test_import_archive_nothing_to_import(self, tmp_path, monkeypatch): 360 360 """Test import_archive when all segments already synced.""" ··· 368 368 369 369 # Set up journal with matching content 370 370 journal_path = tmp_path / "journal" 371 - segment_dir = journal_path / "20250101" / "120000_300" 371 + segment_dir = journal_path / "chronicle" / "20250101" / "120000_300" 372 372 segment_dir.mkdir(parents=True) 373 373 (segment_dir / "audio.flac").write_bytes(content) 374 374 ··· 440 440 441 441 def _setup_journal(self, tmp_path, *, include_stream_json: bool = False) -> Path: 442 442 journal = tmp_path / "journal" 443 - day_dir = journal / "20250103" / "default" / "120000_300" 443 + day_dir = journal / "chronicle" / "20250103" / "default" / "120000_300" 444 444 day_dir.mkdir(parents=True) 445 445 (day_dir / "audio.flac").write_bytes(b"audio data") 446 446 (day_dir / "transcript.jsonl").write_text('{"text": "hello"}\n') ··· 507 507 508 508 journal_root = tmp_path / "journal" 509 509 journal_root.mkdir() 510 - (journal_root / "20250101").mkdir() 511 - (journal_root / "20250103").mkdir() 510 + (journal_root / "chronicle" / "20250101").mkdir(parents=True) 511 + (journal_root / "chronicle" / "20250103").mkdir(parents=True) 512 512 (journal_root / "config").mkdir() 513 513 (journal_root / "streams").mkdir() 514 514 ··· 556 556 journal = self._setup_journal(tmp_path) 557 557 self._set_journal_override(monkeypatch, journal) 558 558 559 - segment_dir = journal / "20250103" / "default" / "120000_300" 559 + segment_dir = journal / "chronicle" / "20250103" / "default" / "120000_300" 560 560 remote_files = [ 561 561 { 562 562 "name": "audio.flac", ··· 655 655 journal = self._setup_journal(tmp_path) 656 656 self._set_journal_override(monkeypatch, journal) 657 657 658 - segment_dir = journal / "20250103" / "default" / "120000_300" 658 + segment_dir = journal / "chronicle" / "20250103" / "default" / "120000_300" 659 659 remote_files = [ 660 660 { 661 661 "name": "audio.flac",
+2 -5
tests/test_updated_days.py
··· 5 5 6 6 import time 7 7 8 - import think.utils 9 8 from think.utils import updated_days 10 9 11 10 12 11 def test_updated_days_fixture(monkeypatch): 13 12 """20250101 has stream.updated but no daily.updated — should be updated.""" 14 13 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "tests/fixtures/journal") 15 - monkeypatch.setattr(think.utils, "_journal_path_cache", None) 16 14 days = updated_days() 17 15 assert "20250101" in days 18 16 ··· 20 18 def test_updated_days_exclude(monkeypatch): 21 19 """Excluded days should not appear in results.""" 22 20 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "tests/fixtures/journal") 23 - monkeypatch.setattr(think.utils, "_journal_path_cache", None) 24 21 days = updated_days(exclude={"20250101"}) 25 22 assert "20250101" not in days 26 23 ··· 28 25 def test_updated_days_clean(tmp_path, monkeypatch): 29 26 """Day with daily.updated newer than stream.updated is not updated.""" 30 27 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 31 - day_dir = tmp_path / "20260101" / "health" 28 + day_dir = tmp_path / "chronicle" / "20260101" / "health" 32 29 day_dir.mkdir(parents=True) 33 30 (day_dir / "stream.updated").touch() 34 31 time.sleep(0.05) ··· 39 36 def test_updated_days_no_stream(tmp_path, monkeypatch): 40 37 """Day without stream.updated is not updated (no stream data).""" 41 38 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 42 - (tmp_path / "20260101").mkdir() 39 + (tmp_path / "chronicle" / "20260101").mkdir(parents=True) 43 40 assert updated_days() == []
-1
tests/test_validate_key.py
··· 4 4 from __future__ import annotations 5 5 6 6 import json 7 - from pathlib import Path 8 7 from unittest.mock import Mock, patch 9 8 10 9 import pytest
+2 -2
think/awareness.py
··· 15 15 16 16 from __future__ import annotations 17 17 18 + import fcntl 18 19 import json 19 20 import logging 20 21 import os 21 22 import tempfile 22 23 import time 23 - import fcntl 24 24 from datetime import datetime 25 25 from pathlib import Path 26 26 from typing import Any ··· 268 268 bool 269 269 True if the section was found and updated, False otherwise. 270 270 """ 271 - from think.utils import get_journal 272 271 from think.entities.core import atomic_write 272 + from think.utils import get_journal 273 273 274 274 file_path = Path(get_journal()) / "sol" / filename 275 275 lock_path = file_path.parent / f".{filename}.lock"
+22 -4
think/formatters.py
··· 40 40 from pathlib import Path 41 41 from typing import Any, Callable 42 42 43 - from think.utils import DATE_RE, get_journal 43 + from think.utils import CHRONICLE_DIR, DATE_RE, get_journal, journal_relative_path 44 44 45 45 46 46 def extract_path_metadata(rel_path: str) -> dict[str, str]: ··· 202 202 "apps/*/agents/*.md": ("think.markdown", "format_markdown", True), 203 203 } 204 204 205 + _DAY_ROOTED_PATTERNS = [p for p in FORMATTERS if p.startswith("*/")] 206 + _STRUCTURAL_PATTERNS = [p for p in FORMATTERS if not p.startswith("*/")] 207 + 205 208 206 209 def get_formatter(file_path: str) -> Callable | None: 207 210 """Return formatter function for a journal-relative file path. ··· 271 274 """ 272 275 files: dict[str, str] = {} 273 276 journal_path = Path(journal) 277 + day_root = ( 278 + journal_path / CHRONICLE_DIR 279 + if (journal_path / CHRONICLE_DIR).is_dir() 280 + else journal_path 281 + ) 274 282 275 - for pattern, (_mod, _func, indexed) in FORMATTERS.items(): 283 + for pattern in _STRUCTURAL_PATTERNS: 284 + _mod, _func, indexed = FORMATTERS[pattern] 276 285 if not indexed: 277 286 continue 278 287 for match in journal_path.glob(pattern): 279 288 if match.is_file(): 280 - rel = str(match.relative_to(journal_path)) 289 + rel = match.relative_to(journal_path).as_posix() 290 + files[rel] = str(match) 291 + 292 + for pattern in _DAY_ROOTED_PATTERNS: 293 + _mod, _func, indexed = FORMATTERS[pattern] 294 + if not indexed: 295 + continue 296 + for match in day_root.glob(pattern): 297 + if match.is_file(): 298 + rel = match.relative_to(day_root).as_posix() 281 299 files[rel] = str(match) 282 300 283 301 return files ··· 314 332 if not file_path.is_relative_to(journal_path): 315 333 raise ValueError(f"File is outside journal directory: {file_path}") 316 334 317 - rel_path = str(file_path.relative_to(journal_path)) 335 + rel_path = journal_relative_path(journal_path, file_path) 318 336 319 337 formatter = get_formatter(rel_path) 320 338 if formatter is None:
+3 -2
think/hooks.py
··· 183 183 Relative path like "20240101/agents/meetings.md". 184 184 """ 185 185 from think.talent import get_output_name 186 - from think.utils import get_journal 186 + from think.utils import CHRONICLE_DIR, get_journal 187 187 188 188 day = context.get("day", "") 189 189 output_path = context.get("output_path", "") ··· 191 191 journal = get_journal() 192 192 193 193 try: 194 - return os.path.relpath(output_path, journal) 194 + rel = os.path.relpath(output_path, journal).replace("\\", "/") 195 + return rel.removeprefix(f"{CHRONICLE_DIR}/") 195 196 except ValueError: 196 197 segment = context.get("segment") 197 198 output_name = get_output_name(name)
+3 -1
think/importers/utils.py
··· 13 13 import re 14 14 from pathlib import Path 15 15 16 + from think.utils import resolve_journal_path 17 + 16 18 # ============================================================================ 17 19 # File Operations 18 20 # ============================================================================ ··· 450 452 for file_path_str in all_files: 451 453 file_path = Path(file_path_str) 452 454 if not file_path.exists(): 453 - file_path = journal_root / file_path_str 455 + file_path = resolve_journal_path(journal_root, file_path_str) 454 456 if not file_path.exists(): 455 457 continue 456 458
+30 -11
think/indexer/journal.py
··· 36 36 load_jsonl, 37 37 ) 38 38 from think.markdown import format_markdown 39 - from think.utils import DATE_RE, get_journal, now_ms, segment_key, segment_parse 39 + from think.utils import ( 40 + CHRONICLE_DIR, 41 + DATE_RE, 42 + get_journal, 43 + journal_relative_path, 44 + now_ms, 45 + resolve_journal_path, 46 + segment_key, 47 + segment_parse, 48 + ) 40 49 41 50 logger = logging.getLogger(__name__) 42 51 ··· 243 252 """Find all signal source files (KG markdown + event JSONL).""" 244 253 journal_path = Path(journal) 245 254 files: dict[str, tuple[str, str]] = {} 255 + day_root = ( 256 + journal_path / CHRONICLE_DIR 257 + if (journal_path / CHRONICLE_DIR).is_dir() 258 + else journal_path 259 + ) 246 260 247 - for path in journal_path.glob("*/agents/knowledge_graph.md"): 261 + for path in day_root.glob("*/agents/knowledge_graph.md"): 248 262 if path.is_file(): 249 - rel = path.relative_to(journal_path).as_posix() 263 + rel = path.relative_to(day_root).as_posix() 250 264 files[rel] = (str(path), "kg") 251 265 252 266 for path in journal_path.glob("facets/*/events/*.jsonl"): ··· 502 516 503 517 def _extract_signal_kg(journal: str, rel_path: str) -> list[dict[str, Any]]: 504 518 """Extract KG appearance and edge signals from a knowledge graph markdown file.""" 505 - abs_path = os.path.join(journal, rel_path) 519 + abs_path = resolve_journal_path(journal, rel_path) 506 520 day = rel_path.split("/")[0] 507 521 508 522 try: ··· 904 918 if os.path.isabs(file_path): 905 919 abs_path = Path(file_path).resolve() 906 920 else: 907 - abs_path = (journal_path / file_path).resolve() 921 + abs_path = resolve_journal_path(journal_path, file_path).resolve() 908 922 909 923 # Validate file exists 910 924 if not abs_path.is_file(): ··· 912 926 913 927 # Validate file is under journal 914 928 try: 915 - rel_path = str(abs_path.relative_to(journal_path)) 929 + rel_path = journal_relative_path(journal_path, abs_path) 916 930 except ValueError: 917 931 raise ValueError(f"File is outside journal directory: {abs_path}") from None 918 932 ··· 942 956 parts = rel_path.replace("\\", "/").split("/") 943 957 if len(parts) >= 4 and segment_key(parts[2]): 944 958 rel_segment = "/".join(parts[:3]) 945 - seg_dir = os.path.join(journal, rel_segment) 959 + seg_dir = str(resolve_journal_path(journal, rel_segment)) 946 960 conn.execute("DELETE FROM chunks WHERE path=?", (rel_segment,)) 947 961 if os.path.isdir(seg_dir): 948 962 seg_stream = _extract_stream(journal, rel_segment + "/dummy") ··· 967 981 parts = rel.replace("\\", "/").split("/") 968 982 # Segment paths: parts[0]=day, parts[1]=stream, parts[2]=segment, parts[3+]=file 969 983 if len(parts) >= 3 and segment_key(parts[2]): 970 - seg_dir = os.path.join(journal, parts[0], parts[1], parts[2]) 984 + seg_dir = str(resolve_journal_path(journal, "/".join(parts[:3]))) 971 985 marker = read_segment_stream(seg_dir) 972 986 if marker: 973 987 return marker.get("stream") ··· 1483 1497 from datetime import datetime 1484 1498 1485 1499 journal_path = Path(journal) 1500 + day_root = ( 1501 + journal_path / CHRONICLE_DIR 1502 + if (journal_path / CHRONICLE_DIR).is_dir() 1503 + else journal_path 1504 + ) 1486 1505 today = datetime.now().strftime("%Y%m%d") 1487 1506 1488 1507 # Collect all matching segment entity files across day/stream/segment dirs 1489 1508 segment_files = [] 1490 - for path in journal_path.glob("**/agents/entities.jsonl"): 1509 + for path in day_root.glob("**/agents/entities.jsonl"): 1491 1510 if not path.is_file(): 1492 1511 continue 1493 1512 try: 1494 - day = path.relative_to(journal_path).parts[0] 1513 + day = path.relative_to(day_root).parts[0] 1495 1514 except (ValueError, IndexError): 1496 1515 continue 1497 1516 if not DATE_RE.fullmatch(day): ··· 1723 1742 1724 1743 seg_count = 0 1725 1744 for rel_segment in sorted(affected_segments): 1726 - segment_dir = os.path.join(journal, rel_segment) 1745 + segment_dir = str(resolve_journal_path(journal, rel_segment)) 1727 1746 conn.execute("DELETE FROM chunks WHERE path=?", (rel_segment,)) 1728 1747 if os.path.isdir(segment_dir): 1729 1748 stream = _extract_stream(journal, rel_segment + "/dummy")
+28 -7
think/merge.py
··· 4 4 """Journal merge engine - one-shot merge of a source journal into the target.""" 5 5 6 6 import json 7 + import logging 7 8 import re 8 9 import shutil 9 10 from dataclasses import dataclass, field ··· 19 20 from think.entities.matching import find_matching_entity 20 21 from think.entities.observations import save_observations 21 22 from think.entities.relationships import save_facet_relationship 22 - from think.utils import iter_segments 23 + from think.utils import CHRONICLE_DIR, iter_segments 23 24 24 25 DATE_RE = re.compile(r"^\d{8}$") 26 + logger = logging.getLogger(__name__) 25 27 26 28 27 29 @dataclass ··· 76 78 77 79 78 80 def _source_day_dirs(source: Path) -> dict[str, Path]: 79 - days: dict[str, Path] = {} 80 - for entry in sorted(source.iterdir()): 81 - if entry.is_dir() and DATE_RE.match(entry.name): 82 - days[entry.name] = entry 83 - return days 81 + chronicle_dir = source / CHRONICLE_DIR 82 + chronicle_days = {} 83 + if chronicle_dir.is_dir(): 84 + chronicle_days = { 85 + entry.name: entry 86 + for entry in chronicle_dir.iterdir() 87 + if entry.is_dir() and DATE_RE.match(entry.name) 88 + } 89 + flat_days = { 90 + entry.name: entry 91 + for entry in source.iterdir() 92 + if entry.is_dir() and DATE_RE.match(entry.name) 93 + } 94 + if chronicle_days and flat_days: 95 + logger.warning( 96 + "Merge source has both flat and %s/ day dirs; preferring %s/.", 97 + CHRONICLE_DIR, 98 + CHRONICLE_DIR, 99 + ) 100 + return chronicle_days 101 + return chronicle_days or flat_days 84 102 85 103 86 104 def _merge_segments( ··· 90 108 dry_run: bool, 91 109 log_path: Path | None = None, 92 110 ) -> None: 111 + target_chronicle = target / CHRONICLE_DIR 112 + if not dry_run: 113 + target_chronicle.mkdir(parents=True, exist_ok=True) 93 114 94 115 for day_name, source_day in sorted(_source_day_dirs(source).items()): 95 - target_day = target / day_name 116 + target_day = target_chronicle / day_name 96 117 for stream, seg_key, seg_path in iter_segments(source_day): 97 118 if stream == "_default": 98 119 target_path = target_day / seg_key
+16 -16
think/prompts.py
··· 32 32 # Cached raw template content loaded from think/templates/*.md 33 33 _templates_cache: dict[str, str] | None = None 34 34 35 - # Cached sol/ template vars loaded from repo and journal sol/ dirs 35 + # Cached repo sol/ template vars loaded from sol/*.md 36 36 _sol_vars_cache: dict[str, str] | None = None 37 37 38 38 SOL_DIR = Path(__file__).parent.parent / "sol" ··· 116 116 Journal sol/ files override repo sol/ files on collision. 117 117 """ 118 118 global _sol_vars_cache 119 - if _sol_vars_cache is not None: 120 - return _sol_vars_cache 121 - 122 119 from think.utils import get_journal 123 120 124 - _sol_vars_cache = {} 121 + if _sol_vars_cache is None: 122 + _sol_vars_cache = {} 125 123 126 - # Repo sol/ first 127 - if SOL_DIR.is_dir(): 128 - for md_path in sorted(SOL_DIR.glob("*.md")): 129 - var_name = f"sol_{md_path.stem}" 130 - try: 131 - post = frontmatter.load(md_path) 132 - _sol_vars_cache[var_name] = post.content.strip() 133 - except Exception: 134 - pass 124 + # Repo sol/ first 125 + if SOL_DIR.is_dir(): 126 + for md_path in sorted(SOL_DIR.glob("*.md")): 127 + var_name = f"sol_{md_path.stem}" 128 + try: 129 + post = frontmatter.load(md_path) 130 + _sol_vars_cache[var_name] = post.content.strip() 131 + except Exception: 132 + pass 133 + 134 + sol_vars = dict(_sol_vars_cache) 135 135 136 136 # Journal sol/ second (wins on collision) 137 137 try: ··· 141 141 var_name = f"sol_{md_path.stem}" 142 142 try: 143 143 post = frontmatter.load(md_path) 144 - _sol_vars_cache[var_name] = post.content.strip() 144 + sol_vars[var_name] = post.content.strip() 145 145 except Exception: 146 146 pass 147 147 except Exception: 148 148 pass 149 149 150 - return _sol_vars_cache 150 + return sol_vars 151 151 152 152 153 153 def reset_sol_vars_cache() -> None:
+20 -18
think/runner.py
··· 5 5 """Unified process spawning and lifecycle management utilities. 6 6 7 7 All subprocess output is automatically logged to: 8 - journal/{YYYYMMDD}/health/{ref}_{process_name}.log 8 + journal/chronicle/{YYYYMMDD}/health/{ref}_{process_name}.log 9 9 10 10 Where process_name is derived from cmd[0] basename, and ref is a unique correlation ID. 11 11 12 12 Symlinks provide stable access paths: 13 - journal/{YYYYMMDD}/health/{process_name}.log (day-level symlink) 13 + journal/chronicle/{YYYYMMDD}/health/{process_name}.log (day-level symlink) 14 14 journal/health/{process_name}.log (journal-level symlink) 15 15 16 16 Logs automatically roll over at midnight for long-running processes. ··· 28 28 from pathlib import Path 29 29 30 30 from think.callosum import CallosumConnection 31 - from think.utils import day_path, get_journal, now_ms 31 + from think.utils import CHRONICLE_DIR, get_journal, now_ms 32 32 33 33 logger = logging.getLogger(__name__) 34 34 ··· 46 46 def _day_health_log_path(day: str, ref: str, name: str) -> Path: 47 47 """Build path to day health log. 48 48 49 - Returns: journal/{day}/health/{ref}_{name}.log 49 + Returns: journal/chronicle/{day}/health/{ref}_{name}.log 50 50 """ 51 - return day_path(day) / "health" / f"{ref}_{name}.log" 51 + return _get_journal_path() / CHRONICLE_DIR / day / "health" / f"{ref}_{name}.log" 52 52 53 53 54 54 def _atomic_symlink(link_path: Path, target: str) -> None: ··· 91 91 When ``day`` is provided, the writer is pinned to that day directory 92 92 and midnight rollover is disabled (batch processing of historical days). 93 93 94 - Writes to: journal/{YYYYMMDD}/health/{ref}_{name}.log 94 + Writes to: journal/chronicle/{YYYYMMDD}/health/{ref}_{name}.log 95 95 96 96 Creates and maintains symlinks: 97 - - journal/{YYYYMMDD}/health/{name}.log -> {ref}_{name}.log (day-level) 98 - - journal/health/{name}.log -> {YYYYMMDD}/health/{ref}_{name}.log (journal-level) 97 + - journal/chronicle/{YYYYMMDD}/health/{name}.log -> {ref}_{name}.log (day-level) 98 + - journal/health/{name}.log -> chronicle/{YYYYMMDD}/health/{ref}_{name}.log (journal-level) 99 99 100 100 When the day changes, automatically closes old file, opens new file, and updates symlinks. 101 101 """ ··· 118 118 def _update_symlinks(self) -> None: 119 119 """Update day-level and journal-level symlinks to point to current log.""" 120 120 journal = _get_journal_path() 121 - day_health = journal / self._current_day / "health" 121 + day_health = journal / CHRONICLE_DIR / self._current_day / "health" 122 122 log_filename = f"{self._ref}_{self._name}.log" 123 123 124 - # Day-level symlink: {YYYYMMDD}/health/{name}.log -> {ref}_{name}.log 124 + # Day-level symlink: chronicle/{YYYYMMDD}/health/{name}.log -> {ref}_{name}.log 125 125 day_symlink = day_health / f"{self._name}.log" 126 126 _atomic_symlink(day_symlink, log_filename) 127 127 128 - # Journal-level symlink: health/{name}.log -> ../{YYYYMMDD}/health/{ref}_{name}.log 129 - # Relative from journal/health/ to journal/{YYYYMMDD}/health/ 128 + # Journal-level symlink: health/{name}.log -> ../chronicle/{YYYYMMDD}/health/{ref}_{name}.log 129 + # Relative from journal/health/ to journal/chronicle/{YYYYMMDD}/health/ 130 130 journal_symlink = journal / "health" / f"{self._name}.log" 131 - relative_target = f"../{self._current_day}/health/{log_filename}" 131 + relative_target = ( 132 + f"../{CHRONICLE_DIR}/{self._current_day}/health/{log_filename}" 133 + ) 132 134 _atomic_symlink(journal_symlink, relative_target) 133 135 134 136 def write(self, message: str) -> None: ··· 172 174 class ManagedProcess: 173 175 """Subprocess wrapper with automatic output logging and lifecycle management. 174 176 175 - All output is automatically logged to: 176 - journal/{YYYYMMDD}/health/{ref}_{name}.log 177 + All output is automatically logged to: 178 + journal/chronicle/{YYYYMMDD}/health/{ref}_{name}.log 177 179 178 180 Where name is derived from cmd[0] basename, and ref is a unique correlation ID. 179 181 180 - Symlinks are automatically created and maintained: 181 - journal/{YYYYMMDD}/health/{name}.log -> {ref}_{name}.log (day-level) 182 - journal/health/{name}.log -> {YYYYMMDD}/health/{ref}_{name}.log (journal-level) 182 + Symlinks are automatically created and maintained: 183 + journal/chronicle/{YYYYMMDD}/health/{name}.log -> {ref}_{name}.log (day-level) 184 + journal/health/{name}.log -> chronicle/{YYYYMMDD}/health/{ref}_{name}.log (journal-level) 183 185 184 186 Logs roll over automatically at midnight for long-running processes. 185 187
+1 -2
think/segment.py
··· 569 569 if succ_path: 570 570 succ_marker = read_segment_stream(succ_path) 571 571 if succ_marker: 572 - old_prev = f"{succ_marker.get('prev_day')}/{stream}/{succ_marker.get('prev_segment')}" 573 572 write_segment_stream( 574 573 succ_path, 575 574 succ_marker["stream"], ··· 609 608 _touch_health_marker(to_day) 610 609 print(f" touched health markers: {src_day}, {to_day}") 611 610 if verbose: 612 - print(f" dream will re-run daily agents on both days") 611 + print(" dream will re-run daily agents on both days") 613 612 614 613 # Post-move verify is informational — the move already completed. 615 614 print()
+38 -7
think/utils.py
··· 27 27 from media import MIME_TYPES 28 28 29 29 DATE_RE = re.compile(r"\d{8}") 30 + CHRONICLE_DIR = "chronicle" 30 31 DEFAULT_STREAM = "_default" 31 32 32 33 ··· 122 123 return journal 123 124 124 125 126 + def resolve_journal_path(journal: str | Path, rel: str) -> Path: 127 + """Resolve a chronicle-free journal-relative path to its on-disk location.""" 128 + if not rel: 129 + raise ValueError("rel must be non-empty") 130 + if os.path.isabs(rel): 131 + raise ValueError("rel must be journal-relative") 132 + if "\\" in rel: 133 + raise ValueError("rel must use POSIX separators") 134 + parts = Path(rel).parts 135 + if not parts or any(p in ("", ".", "..") for p in parts): 136 + raise ValueError("rel must not contain empty, '.', or '..' components") 137 + journal_path = Path(journal) 138 + if DATE_RE.fullmatch(parts[0]): 139 + return journal_path / CHRONICLE_DIR / rel 140 + return journal_path / rel 141 + 142 + 143 + def journal_relative_path(journal: str | Path, abs_path: str | Path) -> str: 144 + """Return a chronicle-free journal-relative POSIX path for an absolute path under the journal.""" 145 + journal_path = Path(journal) 146 + file_path = Path(abs_path) 147 + chronicle_root = journal_path / CHRONICLE_DIR 148 + if file_path.is_relative_to(chronicle_root): 149 + return file_path.relative_to(chronicle_root).as_posix() 150 + return file_path.relative_to(journal_path).as_posix() 151 + 152 + 125 153 def day_path(day: Optional[str] = None, *, create: bool = True) -> Path: 126 - """Return absolute path for a day directory within the journal. 154 + """Return absolute path for a day directory within the journal chronicle. 127 155 128 156 Parameters 129 157 ---------- ··· 135 163 Returns 136 164 ------- 137 165 Path 138 - Absolute path to the day directory. Directory is created if it doesn't exist. 166 + Absolute path to the day directory in chronicle/. Directory is created if 167 + it doesn't exist. 139 168 140 169 Raises 141 170 ------ ··· 150 179 elif not DATE_RE.fullmatch(day): 151 180 raise ValueError("day must be in YYYYMMDD format") 152 181 153 - path = Path(journal) / day 182 + path = Path(journal) / CHRONICLE_DIR / day 154 183 if create: 155 184 path.mkdir(parents=True, exist_ok=True) 156 185 return path ··· 163 192 ------- 164 193 dict[str, str] 165 194 Mapping of day folder names to their full paths. 166 - Example: {"20250101": "/path/to/journal/20250101", ...} 195 + Example: {"20250101": "/path/to/journal/chronicle/20250101", ...} 167 196 """ 168 - journal = get_journal() 197 + chronicle_dir = Path(get_journal()) / CHRONICLE_DIR 198 + if not chronicle_dir.is_dir(): 199 + return {} 169 200 170 201 days: dict[str, str] = {} 171 - for name in os.listdir(journal): 202 + for name in os.listdir(chronicle_dir): 172 203 if DATE_RE.fullmatch(name): 173 - path = os.path.join(journal, name) 204 + path = os.path.join(chronicle_dir, name) 174 205 if os.path.isdir(path): 175 206 days[name] = path 176 207 return days