Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
580103f
feat(db): add WAL mode, busy_timeout, and user_version migration fram…
senna-lang Jun 10, 2026
917b5ca
feat: connection-leak fix, timeout cleanup, incremental indexing, con…
senna-lang Jun 10, 2026
2d2e2b4
feat(embedding): prevent server double-start and harden socket protocol
senna-lang Jun 10, 2026
bf0fbfd
merge: embedding (H3,H5) and search/llm/index (H4,H6,P2,Q5) lanes
senna-lang Jun 10, 2026
ec23133
feat: distill transactions, distill_status, version tracking, indexes…
senna-lang Jun 10, 2026
008a0ef
feat(hooks): atomic settings.json write and loci hook uninstall (B4, H7)
senna-lang Jun 10, 2026
1928c7d
refactor: flock-based distill lock and tobytes embedding serializatio…
senna-lang Jun 10, 2026
74d4c98
feat(cli): lighten loci context output, add --full flag (C5)
senna-lang Jun 10, 2026
207f13e
merge: lighten loci context output, add --full flag (C5)
senna-lang Jun 10, 2026
9c28c34
feat(distill): repair silent data loss and revive code reverse-lookup…
senna-lang Jun 10, 2026
df07d8c
merge: repair silent data loss and revive code reverse-lookup (C1-C4,C6)
senna-lang Jun 10, 2026
a9b6c0c
refactor: consolidate sha256, name startup-poll constant, test call_c…
senna-lang Jun 11, 2026
a4b7425
docs(readme): document loci hook uninstall and context --full
senna-lang Jun 11, 2026
b61b1af
test(prime): make test_prime_outputs_instructions deterministic
senna-lang Jun 11, 2026
2d8eb30
fix: address Claude review — ply coordinate drift and WAL sidecar perms
senna-lang Jun 11, 2026
6976780
feat(prime): rewrite PRIME_TEXT with agent-action triggers and concre…
senna-lang Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,8 @@ loci hook install # Register hooks to ~/.claude/setti
<!-- BEGIN CODEATRIUM -->
## Past Memory Search (codeatrium)

IMPORTANT: Command usage is injected automatically at session start via `loci prime` (SessionStart hook).
IMPORTANT: Full usage instructions are injected automatically at session start via `loci prime` (SessionStart hook).
If not in context, run `loci prime`.

### Rules

1. **Search before implementing** — always check if something was discussed or built before starting work.
2. **Check symbols when you lack context** — run `loci context --symbol` before changing a function you don't have enough background on.
3. **Use technical terms** — queries with exact symbol names, error messages, or parameter names yield better results.
4. **Follow up with `loci show`** — when `exchange_core` is ambiguous, fetch the full verbatim conversation.
<!-- END CODEATRIUM -->

---
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@ Agent instructions are injected automatically — no manual setup required:
| `loci index` | Index new session logs |
| `loci distill [--limit N]` | Distill undistilled exchanges via LLM |
| `loci search "query" --json` | Semantic search (agent-facing) |
| `loci context --symbol "name" --json` | Code symbol → past conversations |
| `loci context --symbol "name" --json` | Code symbol → past conversations (lightweight; add `--full` for verbatim text) |
| `loci show "<ref>" --json` | Retrieve verbatim conversation |
| `loci status` | Show index state |
| `loci server start/stop/status` | Embedding server management |
| `loci hook install` | Re-register hooks (normally already done by `loci init`) |
| `loci hook uninstall` | Remove codeatrium hooks from `settings.json` |

## Automation (Claude Code Hooks)

Expand Down
6 changes: 4 additions & 2 deletions src/codeatrium/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def init(
try:
con.execute(
f"""
UPDATE exchanges SET distilled_at = 'skipped'
UPDATE exchanges SET distilled_at = 'skipped', distill_status = 'skipped'
WHERE distilled_at IS NULL
AND id IN (
SELECT id FROM exchanges
Expand Down Expand Up @@ -237,14 +237,16 @@ def _on_progress(
else:
typer.echo(f" [{cur}/{tot}] distilled", err=True)

count = distill_all(
count, err_count = distill_all(
db,
model=cfg.distill_model,
on_progress=_on_progress,
project_root=str(root),
distill_min_chars=cfg.distill_min_chars,
)
typer.echo(f"Distilled {count} exchange(s).")
if err_count > 0:
typer.echo(f"{err_count} exchange(s) failed — see errors above.", err=True)
except KeyboardInterrupt:
typer.echo(
"\n⚠ Distillation interrupted. "
Expand Down
46 changes: 21 additions & 25 deletions src/codeatrium/cli/distill_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def distill(
] = None,
) -> None:
"""未蒸留の exchange を claude -p で蒸留して palace_objects を生成する"""
import fcntl
import os

from codeatrium.config import load_config
Expand All @@ -30,31 +31,14 @@ def distill(

lock_path = db.parent / "distill.lock"

# ロック取得: O_CREAT | O_EXCL で原子的に作成(TOCTOU 防止)
# ロック取得: fcntl.flock で排他ロック(LOCK_NB: 非ブロッキング)
fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o600)
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
os.close(fd)
except FileExistsError:
# 既存ロックのプロセスが生きているか確認
try:
existing_pid = int(lock_path.read_text().strip())
os.kill(existing_pid, 0)
typer.echo(
f"loci distill is already running (PID {existing_pid}). Exiting.",
err=True,
)
raise typer.Exit(0)
except (ValueError, ProcessLookupError, PermissionError):
# stale lock — 再取得
lock_path.unlink(missing_ok=True)
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
os.close(fd)
except FileExistsError:
typer.echo("loci distill: lost lock race after stale cleanup. Exiting.", err=True)
raise typer.Exit(0)
typer.echo("loci distill is already running. Exiting.", err=True)
raise typer.Exit(0)

def _on_progress(cur: int, tot: int, error: str | None = None) -> None:
if error:
Expand All @@ -63,7 +47,16 @@ def _on_progress(cur: int, tot: int, error: str | None = None) -> None:
typer.echo(f" [{cur}/{tot}] distilled", err=True)

try:
count = distill_all(
from codeatrium.db import check_drift

drifts = check_drift(db)
for key, recorded, current in drifts:
typer.echo(
f"[warn] {key} changed ({recorded} -> {current}). Re-index recommended.",
err=True,
)

count, err_count = distill_all(
db,
limit=limit,
model=cfg.distill_model,
Expand All @@ -72,5 +65,8 @@ def _on_progress(cur: int, tot: int, error: str | None = None) -> None:
distill_min_chars=cfg.distill_min_chars,
)
typer.echo(f"Distilled {count} exchange(s).")
if err_count > 0:
typer.echo(f"{err_count} exchange(s) failed — see errors above.", err=True)
finally:
lock_path.unlink(missing_ok=True)
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
9 changes: 9 additions & 0 deletions src/codeatrium/cli/hook_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ def hook_install() -> None:
cfg = load_config(find_project_root())
_changed, message = install_hooks(batch_limit=cfg.distill_batch_limit)
typer.echo(message)


@hook_app.command("uninstall")
def hook_uninstall() -> None:
"""Claude Code の settings.json から codeatrium 関連フックをすべて除去する。"""
from codeatrium.hooks import uninstall_hooks

_changed, message = uninstall_hooks()
typer.echo(message)
4 changes: 4 additions & 0 deletions src/codeatrium/cli/index_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def index(
raise typer.Exit(1)

init_db(db)
from codeatrium.db import check_drift
drifts = check_drift(db)
for key, recorded, current in drifts:
typer.echo(f"[warn] {key} changed ({recorded} -> {current}). Re-index recommended.", err=True)
cfg = load_config(root)

target_dir = path or resolve_claude_projects_path(root)
Expand Down
42 changes: 19 additions & 23 deletions src/codeatrium/cli/prime_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,40 @@
PRIME_TEXT = """\
## Past Memory Search (codeatrium)

Use `loci search` to find past implementations, decisions, and code locations.
codeatrium records every past conversation turn, decision, and code location. Retrieve that memory **before** acting — not after.

### When to use
### When to act (agent-initiated triggers)

- When asked "where did we implement X?" or "where is X?"
- When checking if a similar bug was fixed before
- When verifying if a feature already exists
- When looking up the reasoning behind a past design decision
- Before editing code you lack context about — use `loci context --symbol` to review past discussions
- Before refactoring or changing the behavior of a function — use `loci context --symbol` to check past design decisions
- **Before editing or refactoring a function** — recall past design decisions and known constraints for that symbol.
- **Before starting a new implementation** — check if similar work was done before; reuse decisions and avoid re-debating settled choices.
- **When you encounter a known or recurring error** — search for past fixes; the solution may already be documented.

### Commands
### Search — semantic query over past conversations

```bash
# Semantic search
loci search "query" --json --limit 5
# Find past discussions, decisions, or implementations
loci search "BM25 RRF fusion ranking" --json --limit 5

# Reverse lookup: code symbol -> past conversations
loci context --symbol "symbol_name" --json

# Retrieve verbatim conversation (use verbatim_ref from search results)
# Retrieve verbatim exchange (use verbatim_ref from search results)
loci show "<verbatim_ref>" --json
```

### Context — reverse lookup from code symbol to past conversations

Touching a symbol = recalling memory about that symbol. Before changing any function or class, look up what was decided about it.

```bash
# Retrieve all past conversations that involved this symbol
loci context --symbol "SymbolResolver.extract" --json
```\
"""

CLAUDE_MD_SECTION = f"""\
{BEGIN_MARKER}
## Past Memory Search (codeatrium)

IMPORTANT: Command usage is injected automatically at session start via `loci prime` (SessionStart hook).
IMPORTANT: Full usage instructions are injected automatically at session start via `loci prime` (SessionStart hook).
If not in context, run `loci prime`.

### Rules

1. **Search before implementing** — always check if something was discussed or built before starting work.
2. **Check symbols when you lack context** — run `loci context --symbol` before changing a function you don't have enough background on.
3. **Use technical terms** — queries with exact symbol names, error messages, or parameter names yield better results.
4. **Follow up with `loci show`** — when `exchange_core` is ambiguous, fetch the full verbatim conversation.
{END_MARKER}\
"""

Expand Down
26 changes: 19 additions & 7 deletions src/codeatrium/cli/search_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def search(
typer.echo("Not initialized. Run `loci init` first.", err=True)
raise typer.Exit(1)

from codeatrium.db import check_drift
drifts = check_drift(db)
for key, recorded, current in drifts:
typer.echo(f"[warn] {key} changed ({recorded} -> {current}). Re-index recommended.", err=True)

embedder = Embedder()
query_vec = embedder.embed(query)
results = search_combined(db, query, query_vec, limit=limit)
Expand Down Expand Up @@ -62,6 +67,7 @@ def context(
],
limit: Annotated[int, typer.Option("--limit", "-n", help="返す件数")] = 5,
json_output: Annotated[bool, typer.Option("--json", help="JSON で出力")] = False,
full: Annotated[bool, typer.Option("--full", help="全文(user_content / agent_content)を含める")] = False,
) -> None:
"""シンボル名から関連する過去会話を逆引きする"""
from codeatrium.db import get_connection
Expand All @@ -87,10 +93,13 @@ def context(
e.user_content,
e.agent_content,
p.exchange_core,
p.specific_context
p.specific_context,
c.source_path,
e.ply_start
FROM symbols s
JOIN palace_objects p ON p.id = s.palace_object_id
JOIN exchanges e ON e.id = p.exchange_id
JOIN conversations c ON c.id = e.conversation_id
WHERE s.symbol_name LIKE ?
LIMIT ?
""",
Expand All @@ -103,8 +112,9 @@ def context(
return

if json_output:
output = [
{
output = []
for r in rows:
base = {
"symbol_name": r["symbol_name"],
"symbol_kind": r["symbol_kind"],
"file_path": r["file_path"],
Expand All @@ -113,11 +123,12 @@ def context(
"exchange_id": r["exchange_id"],
"exchange_core": r["exchange_core"],
"specific_context": r["specific_context"],
"user_content": r["user_content"],
"agent_content": r["agent_content"],
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
}
for r in rows
]
if full:
base["user_content"] = r["user_content"]
base["agent_content"] = r["agent_content"]
output.append(base)
typer.echo(json.dumps(output, ensure_ascii=False, indent=2))
else:
for i, r in enumerate(rows, 1):
Expand All @@ -126,3 +137,4 @@ def context(
typer.echo(f" {r['signature']}")
if r["exchange_core"]:
typer.echo(f" Core: {r['exchange_core']}")
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
17 changes: 16 additions & 1 deletion src/codeatrium/cli/server_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

server_app = typer.Typer(help="embedding サーバー管理")

_SERVER_STARTUP_POLL_ATTEMPTS: int = 150 # サーバー起動確認のポーリング回数(0.2秒 × 150 = 最大30秒待機)


@server_app.command("start")
def server_start() -> None:
"""embedding サーバーをバックグラウンドで起動する"""
import json as _json
import os
import socket as _socket
import subprocess

Expand All @@ -36,8 +39,20 @@ def server_start() -> None:
return
except Exception:
sock.unlink(missing_ok=True)
server_pid_path(root).unlink(missing_ok=True)

pid_path = server_pid_path(root)
if pid_path.exists():
try:
_pid = int(pid_path.read_text().strip())
os.kill(_pid, 0)
except (ProcessLookupError, ValueError):
# プロセス不在 or 不正な PID → stale とみなして除去
pid_path.unlink(missing_ok=True)
except PermissionError:
# 別ユーザーのプロセスが生存 → 触らない
pass

proc = subprocess.Popen(
[_loci_python(), "-m", "codeatrium.embedder_server", str(sock)],
stdout=subprocess.DEVNULL,
Expand All @@ -48,7 +63,7 @@ def server_start() -> None:

import time

for i in range(150):
for i in range(_SERVER_STARTUP_POLL_ATTEMPTS):
if sock.exists():
typer.echo(f"Server started (PID {proc.pid})")
return
Expand Down
21 changes: 18 additions & 3 deletions src/codeatrium/cli/status_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,26 @@ def status(
con = get_connection(db)
total = con.execute("SELECT COUNT(*) FROM exchanges").fetchone()[0]
distilled = con.execute(
"SELECT COUNT(*) FROM exchanges WHERE distilled_at IS NOT NULL"
"SELECT COUNT(*) FROM exchanges WHERE distill_status = 'distilled'"
).fetchone()[0]
skipped = con.execute(
"SELECT COUNT(*) FROM exchanges WHERE distill_status = 'skipped'"
).fetchone()[0]
pending = con.execute(
"SELECT COUNT(*) FROM exchanges WHERE distill_status = 'pending'"
).fetchone()[0]
palace_count = con.execute("SELECT COUNT(*) FROM palace_objects").fetchone()[0]
symbol_count = con.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
con.close()

from codeatrium.db import check_drift
drifts = check_drift(db)
for key, recorded, current in drifts:
typer.echo(
f"[drift] {key}: recorded={recorded}, current={current} — re-index recommended",
err=True,
)

db_size_bytes = db.stat().st_size
db_size_kb = db_size_bytes / 1024

Expand All @@ -41,7 +55,8 @@ def status(
"db_path": str(db),
"exchanges": total,
"distilled": distilled,
"undistilled": total - distilled,
"skipped": skipped,
"pending": pending,
"palace_objects": palace_count,
"symbols": symbol_count,
"db_size_kb": round(db_size_kb, 1),
Expand All @@ -53,7 +68,7 @@ def status(
else:
typer.echo(f"DB: {db} ({db_size_kb:.1f} KB)")
typer.echo(
f"Exchanges : {total} total, {distilled} distilled, {total - distilled} pending"
f"Exchanges : {total} total | {distilled} distilled, {skipped} skipped, {pending} pending"
)
typer.echo(f"Palace : {palace_count}")
typer.echo(f"Symbols : {symbol_count}")
Loading
Loading