Skip to content

feat(journal): add tempyr-journal capture layer + journal_log MCP/CLI#20

Merged
cleak merged 7 commits into
masterfrom
claude/blissful-einstein-52c94d
Apr 28, 2026
Merged

feat(journal): add tempyr-journal capture layer + journal_log MCP/CLI#20
cleak merged 7 commits into
masterfrom
claude/blissful-einstein-52c94d

Conversation

@cleak
Copy link
Copy Markdown
Owner

@cleak cleak commented Apr 28, 2026

Summary

Adds a new tempyr-journal crate that captures agent reasoning (decisions, dead ends, findings, etc.) as append-only JSONL under <git-common-dir>/tempyr/journals/open/. Survives worktree abandonment because it lives in the shared Git directory. Exposed via journal_log MCP tool and tempyr journal log CLI.

This is the capture layer of a 4-phase plan. Publisher (Git refs + push), search/index, and integration with the existing graph follow in subsequent PRs.

What's in here

  • 8-kind taxonomy: plan | finding | assumption | question | decision | dead_end | risk | outcome with per-kind structured field validation (e.g., decision requires chosen/rationale/reversible; dead_end requires approach/failure_mode).
  • Cross-platform append-only JSONL writer using stable std::fs::File::lock (Rust 1.89+, no third-party crate). Windows-correct open flags (.read(true).append(true)); 8-thread concurrent-write stress test passes with no torn lines.
  • git rev-parse --git-common-dir resolution so journals are written to the primary repo's .git/, not a worktree's .git gitfile.
  • Built-in redaction: 9 gitleaks-style regex rules (Anthropic/OpenAI/GitHub/AWS/Slack tokens, JWTs, private keys, DB URLs with passwords, user-home paths) plus Shannon-entropy fallback. Default mode blocks the write.
  • Schema versioned (v: 1) for forward compat; readers ignore unknown fields.
  • Session lifecycle: <YYYYMMDD>-<wt_hash>-<HHMMSS> ID, atomic meta.json sidecar (create_new), .ready marker for the publisher.
  • 78 new unit tests (317 workspace-wide); end-to-end smoke test verified writing real entries from this worktree to the primary repo's .git/tempyr/journals/.

Crate layout

Module Purpose
entry Entry struct + Confidence/Severity/Polarity enums with FromStr impls
kind Kind enum, parse_helpful with Levenshtein typo suggestions, per-kind validation
path git_common_dir / repo_toplevel / worktree_hash (Windows case-folded) + journal directory layout
session SessionId (regex-validated, path-injection-safe), Session lifecycle
writer append(session, entry) — validate → lock → single write_all → fsync
redact Redactor with Block/Redact/Warn modes; static default_redactor()
state PublisherState (sticky last-error/counters, atomic temp+rename) + rotating event log

Out of scope (subsequent PRs)

  • Publisher (tempyr journal flush, in-process tokio ticker, Git tree/commit/push to refs/tempyr/journals/..., public-repo detection at init, refspec config, pack-refs cadence)
  • Search index (SQLite + FTS5 + sqlite-vec, RRF ranking, journal_search MCP tool, tempyr journal search CLI)
  • Auto-emit on task status transitions and interview lifecycle
  • .claude/settings.json hook template for SessionStart/SessionEnd
  • MCP annotations across all existing tempyr tools

Test plan

  • cargo test --workspace --lib — 317 pass
  • cargo clippy --workspace --all-targets — clean
  • cargo fmt --check — clean
  • End-to-end: tempyr journal log decision "..." --chosen X --rationale Y --reversible true writes a valid JSONL line to the primary repo's .git/tempyr/journals/open/, with branch and HEAD auto-captured
  • End-to-end: tempyr journal log baddkindd "..." returns Unknown kind "baddkindd". Valid kinds: plan, finding, ...
  • End-to-end: tempyr journal log desicion "..." returns Unknown kind "desicion". Did you mean + "decision?"
  • Concurrent-writer stress test (8 threads × 32 appends) — no torn lines
  • Redaction blocks Anthropic/OpenAI/GitHub/AWS keys, JWTs, private keys, DB URLs with passwords, and high-entropy base64 blobs (18 redaction tests)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full session journaling: append-only JSONL entries with generated session IDs/metadata, multiple validated entry kinds (with helpful parsing/validation), atomic concurrent appends, session resume/finalize semantics, repo-aware storage paths, publisher state/logging with rotation, and configurable secret redaction modes (block/redact/warn).
    • CLI: new journal command to append structured entries (rich fields, JSON/plain output, optional finalize).
    • MCP: new journal logging tool and configurable agent attribution.
  • Documentation

    • Architecture docs updated to include the new journal component and workspace reshaping.
  • Chores

    • Workspace extended to include the new journal crate and shared dependencies.

cleak and others added 2 commits April 27, 2026 21:15
New tempyr-journal crate with append-only JSONL writer, 8-kind taxonomy
(plan/finding/assumption/question/decision/dead_end/risk/outcome), per-kind
validation, gitleaks-style redaction with Shannon-entropy fallback, sticky
publisher state, and rotating event log. Capture entry points: journal_log
MCP tool and `tempyr journal log` CLI. Entries land in
`<git-common-dir>/tempyr/journals/open/` so they survive worktree
abandonment via the shared Git directory; publisher and search come in
subsequent phases.

78 new unit tests (317 workspace-wide), end-to-end smoke test verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-pass cleanup driven by /simplify and a manual review:

- Lift `parse_confidence`/`parse_severity`/`parse_polarity` into `FromStr`
  impls on `Confidence`/`Severity`/`Polarity`; both MCP and CLI use one
  `parse_opt::<T>` helper.
- Replace local Levenshtein with `strsim::levenshtein` (already a workspace
  member crate dep; promoted to workspace).
- Use `default_redactor()` static (`OnceLock<Redactor>`) instead of rebuilding
  9 regexes per write.
- Add `Entry::for_session(kind, summary, &session)` so production paths and
  test fixtures stop hand-constructing 25-field literals.
- Restructure CLI: `JournalAction::Log(Box<LogArgs>)` via `#[derive(Args)]` —
  removes a 23-positional `run_log` and the 22-field destructure in `main.rs`,
  also fixes a clippy::large_enum_variant.
- Drop dead `TestResults` schema and `SessionMeta.normalized_worktree_path`
  (the latter duplicated `repo_root`).
- Replace hand-rolled byte-class checks for `is_valid_session_id` with a
  cached regex.
- Fix two TOCTOU patterns in `Session::resume` and `PublisherState::load`.
- Fix misleading `--reversible/--no-reversible` docstring (clap-derive only
  supports `--reversible <true|false>` for `Option<bool>`).
- Improve `UnknownKind` error format so close-match and full-list cases both
  read naturally.
- Strengthen `worktree_common_dir_points_to_primary` test to actually verify
  the worktree-vs-primary differentiation it claims.
- Update `CLAUDE.md` and `AGENTS.md` to document the new crate.

317 tests still pass; clippy and fmt clean. Net -60 LOC.

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

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new tempyr-journal crate and integrates it into workspace, CLI, and MCP. Implements append-only JSONL session journaling with entry schema/validation, kind-specific fields, Git-aware path/session resolution, secret redaction, publisher state, and a concurrency-safe locked writer; docs/manifests updated.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, CLAUDE.md
Docs updated to describe the new tempyr-journal crate and revised workspace dependency layering (eight crates).
Workspace & Manifests
Cargo.toml, crates/tempyr-interview/Cargo.toml, crates/tempyr-mcp/Cargo.toml, crates/tempyr-cli/Cargo.toml, crates/tempyr-journal/Cargo.toml
Added crates/tempyr-journal to workspace members; migrated/added workspace dependencies (regex, strsim, chrono); tempyr-cli and tempyr-mcp now depend on tempyr-journal.
CLI: command wiring
crates/tempyr-cli/src/main.rs, crates/tempyr-cli/src/commands/mod.rs
New journal subcommand and routing to the journal handler added.
CLI: journal command
crates/tempyr-cli/src/commands/journal_cmd.rs
New LogArgs and run_log: parse args, resolve git/common-dir, open or resume Session for (worktree,agent), build EntryDraft, parse typed fields (confidence/severity/polarity), call writer, emit JSON/plain output.
Journal crate root & API
crates/tempyr-journal/src/lib.rs
Public crate root exposing modules, types, error enum, and writer API re-exports.
Entry schema
crates/tempyr-journal/src/entry.rs
Defines Entry struct, enums (Confidence,Severity,Polarity), ID generation, serde field naming/omission, and unit tests.
Kind logic & validation
crates/tempyr-journal/src/kind.rs
Kind enum, friendly parsing with Levenshtein suggestions, descriptions, per-kind validation rules (validate_entry), and tests.
Path helpers
crates/tempyr-journal/src/path.rs
Git common-dir/toplevel/branch/HEAD discovery, worktree hashing, repo-relative normalization, journal layout path constructors, and ensure-layout helper with tests.
Session lifecycle
crates/tempyr-journal/src/session.rs
SessionId, SessionMeta, Session open/resume/find/finalize semantics, atomic meta writes with retry, resume consistency checks, and tests.
Redaction
crates/tempyr-journal/src/redact.rs
Regex + entropy-based redactor with modes (Block,Redact,Warn), check/enforce, configuration API, and default_redactor.
Publisher state & log
crates/tempyr-journal/src/state.rs
Publisher state persistence (atomic writes) and append-only JSON-per-line publisher.log with rotation and APIs.
Append writer
crates/tempyr-journal/src/writer.rs
Locked, atomic append of newline-terminated JSONL with sync; write_entry/append/append_validated pipeline and concurrency tests.
MCP integration
crates/tempyr-mcp/src/handler.rs
New JournalLogParams and journal_log tool: parse params, construct EntryDraft, call write_entry; TempyrServer caches Session per repo/worktree and gains configurable agent attribution.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "tempyr CLI"
    participant MCP as "Tempyr MCP"
    participant Git as "git"
    participant Redactor as "default_redactor()"
    participant Session as "Session"
    participant Writer as "writer::append"
    participant FS as "Filesystem"

    CLI->>Git: discover common-dir, worktree top, branch/HEAD
    CLI->>Session: open_or_resume(common_dir, worktree_top, agent)
    CLI->>Redactor: enforce(entry)
    alt not redacted
        CLI->>Writer: write_entry(session, draft)
        Writer->>FS: open/lock JSONL, write newline JSON, sync, release lock
        Writer-->>CLI: success (entry id, session id, finalized?)
    else redacted and mode=Block
        Redactor-->>CLI: return Redacted error
    end

    MCP->>Git: discover common-dir, worktree top
    MCP->>Session: cached open_or_resume or replace on repo change
    MCP->>Redactor: enforce(entry)
    MCP->>Writer: write_entry(session, draft)
    Writer->>FS: lock/write/sync
    Writer-->>MCP: success (entry id, session id, finalized?)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

  • cleak/blueberry#302: Shares changes around using Git's common-dir for shared open-session buffers and journal path resolution.
  • Merge MCP server into the tempyr binary #2: Overlaps MCP/CLI integration and TempyrServer session handling changes relevant to journaling.

Poem

"I hop and scrawl in JSON lines tonight,
Locks click softly, entries tucked in tight,
j-ids glimmer under branch and light,
CLI and MCP keep the journal right,
A rabbit redacts what shouldn't meet the sight." 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding a new tempyr-journal crate with journal_log MCP and CLI commands to capture agent reasoning.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tempyr-cli/src/commands/journal_cmd.rs`:
- Around line 95-108: The CLI file paths in entry.files must be repo-normalized
before redaction: for each path coming from args.files (used after
Entry::for_session and assigned to entry.files), convert absolute paths under
worktree_top into relative paths (use the same logic as
cwd.strip_prefix(&worktree_top).ok().map(|p| p.to_string_lossy().replace('\\',
"/'))), normalize separators, and leave non-worktree paths unchanged (or keep
as-is) so that redact.rs sees normalized repo-relative entries instead of raw
home/absolute paths; assign the resulting normalized Vec<String> to entry.files
instead of copying args.files directly.

In `@crates/tempyr-journal/src/kind.rs`:
- Around line 126-181: Trim surrounding whitespace before measuring or
emptiness-checking user strings in validate_entry: use trimmed lengths for
summary and detail (e.g., compute len from entry.summary.trim().chars().count()
and detail_len from entry.detail.as_ref().map(|d|
d.trim().chars().count()).unwrap_or(0)) and replace the blank closure to treat
strings that are only whitespace as empty (e.g., blank should use
as_deref().map(|s| s.trim().is_empty()).unwrap_or(true)); keep the existing
error branches (Decision, DeadEnd, Assumption) but perform these trimmed checks
so fields like chosen, rationale, approach, and failure_mode composed only of
spaces fail validation.

In `@crates/tempyr-journal/src/path.rs`:
- Around line 165-170: The function canonicalize_or_keep currently returns
Result<PathBuf> but never returns Err; change its signature to return PathBuf
and update its body to return the PathBuf directly (return strip_unc(c) or
p.to_path_buf()); then update all callers that use canonicalize_or_keep
(references in the diff: calls that currently use the `?` operator) to remove
the `?` and accept the PathBuf return type (adjust any surrounding code that
expected a Result accordingly).

In `@crates/tempyr-journal/src/session.rs`:
- Around line 182-208: On collision when OpenOptions::create_new returns
AlreadyExists, don't silently ignore it: read the existing file at meta_path,
parse it with serde_json::from_str (or from_reader) and replace the in-memory
meta variable so the returned Session reflects the persisted state; propagate
any read/parse errors instead of returning success with the original meta.
Update the match in the function that writes the metadata (the block using
meta_path, serde_json::to_string_pretty, and OpenOptions) to handle Err(e) if
e.kind() == std::io::ErrorKind::AlreadyExists by opening meta_path,
deserializing into meta, and then continue to construct and return Session {
common_dir: common_dir.to_path_buf(), meta } (or alternatively return a distinct
collision result if you prefer).
- Around line 210-224: The resume function should validate that the deserialized
SessionMeta corresponds to the requested session_id to detect tampering or
corruption: after parsing SessionMeta (symbol SessionMeta) in resume, compare
meta.session_id (or the field that holds the id inside SessionMeta) to the
session_id parameter used to build jpath::session_meta_path and return an Err
describing the mismatch (rather than silently returning a Session) if they
differ; update the return path in resume (which constructs Session { common_dir,
meta }) to only proceed when the IDs match.

In `@crates/tempyr-mcp/src/handler.rs`:
- Around line 1724-1727: The code assigns entry.files =
p.files.unwrap_or_default() without normalizing repo-relative paths, which lets
absolute user-home paths slip through and later cause redact enforcement to
fail; update the assignment to map/convert each path in p.files (when present)
into a worktree-relative string (using worktree_top and the existing
relative_cwd/relative path helper logic) before setting entry.files so that
default_redactor().enforce sees repo-local paths; ensure you reuse the same
normalization used for cwd (relative_cwd or equivalent) and handle
unwrap_or_default() to avoid panics.
- Around line 1716-1719: The code currently uses env::current_dir() (cwd) and
jpath::git_common_dir / jpath::repo_toplevel directly, bypassing find_project()
and the anchoring logic; change it to resolve the repository from the anchored
project instead: call find_project() (and ensure
project_anchor_state.wait_ready() has completed) to obtain the anchored project
root, then compute common_dir and worktree_top from that anchored root (instead
of cwd) so journal_log and this handler use the same root/env resolution as
other tools; update any uses of the cwd variable to reference the anchored
project's path and preserve the same error mapping semantics.
- Around line 732-748: The current journal_session_or_open returns and caches a
single Session in self.journal_session, causing sessions to be reused across
different repositories/worktrees; change the cache to be keyed by repo/worktree
and update journal_session_or_open to lookup by a key derived from common_dir
and worktree_top (e.g., a PathBuf or string composite), returning the existing
Session if present or creating one via Session::open and inserting it into the
map before returning; adjust references to self.journal_session (replace
Option<Session> with a Mutex<HashMap<Key, Session>>), and ensure clones are used
when returning a Session so multiple callers get owned instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb9173cf-64ec-4fff-b3d2-25fcef4a9683

📥 Commits

Reviewing files that changed from the base of the PR and between 405ec33 and 1bbb180.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • AGENTS.md
  • CLAUDE.md
  • Cargo.toml
  • crates/tempyr-cli/Cargo.toml
  • crates/tempyr-cli/src/commands/journal_cmd.rs
  • crates/tempyr-cli/src/commands/mod.rs
  • crates/tempyr-cli/src/main.rs
  • crates/tempyr-interview/Cargo.toml
  • crates/tempyr-journal/Cargo.toml
  • crates/tempyr-journal/src/entry.rs
  • crates/tempyr-journal/src/kind.rs
  • crates/tempyr-journal/src/lib.rs
  • crates/tempyr-journal/src/path.rs
  • crates/tempyr-journal/src/redact.rs
  • crates/tempyr-journal/src/session.rs
  • crates/tempyr-journal/src/state.rs
  • crates/tempyr-journal/src/writer.rs
  • crates/tempyr-mcp/Cargo.toml
  • crates/tempyr-mcp/src/handler.rs

Comment thread crates/tempyr-cli/src/commands/journal_cmd.rs Outdated
Comment thread crates/tempyr-journal/src/kind.rs
Comment thread crates/tempyr-journal/src/path.rs Outdated
Comment thread crates/tempyr-journal/src/session.rs Outdated
Comment thread crates/tempyr-journal/src/session.rs
Comment thread crates/tempyr-mcp/src/handler.rs
Comment thread crates/tempyr-mcp/src/handler.rs Outdated
Comment thread crates/tempyr-mcp/src/handler.rs Outdated
CodeRabbit flagged 8 inline issues; verified each against current code and
applied fixes plus tests:

- `path.rs`: simplify `canonicalize_or_keep` (always-Ok) to return `PathBuf`,
  drop `?` at callers; add `repo_relative_path(path, worktree_top)` helper.
- `path.rs`: tests covering normalized abs path, relative pass-through, and
  out-of-worktree preservation.
- `kind.rs`: trim user strings before length/empty checks so a string of
  pure whitespace can no longer satisfy `summary >= 20`, `detail >= 50`, or
  the per-kind required `chosen`/`rationale`/`approach`/`failure_mode`
  fields. Tests cover the three previously-permissive cases.
- `session.rs`: on `Session::open_at` AlreadyExists collision, read+parse
  the persisted meta so the returned `Session` reflects disk (was silently
  returning the new caller's args). Existing test extended to assert agent
  comes from disk.
- `session.rs`: `Session::resume` validates `meta.session_id` matches the
  requested id; refuses to return a Session built from a tampered/corrupt
  meta written under the wrong filename. New test.
- `journal_cmd.rs`: normalize `--file` paths through `repo_relative_path`
  before redaction so absolute repo paths under `/Users/<n>/` or
  `C:\Users\<n>\` aren't blocked by the `user_home_path` rule.
- `handler.rs`: same normalization for `journal_log` MCP `files` param.
- `handler.rs`: resolve `common_dir`/`worktree_top` via `find_project()`
  instead of raw `env::current_dir()`, matching every other tool's anchored
  root resolution (waits on `project_anchor_state` too).
- `handler.rs`: cache journal session keyed by `(common_dir, worktree_top)`
  in a `JournalSessionCache` struct, so a server that ends up serving more
  than one repo/worktree doesn't keep appending to the wrong session id.

Smoke-tested: `tempyr journal log finding ... --file <abs path>` now
records `crates/tempyr-journal/src/path.rs` instead of being blocked.
All 457 tests pass; clippy and fmt clean.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tempyr-cli/src/commands/journal_cmd.rs`:
- Around line 92-93: Currently the code always calls Session::open(&common_dir,
&worktree_top, &args.agent) which creates a new timestamped session_id; instead,
before creating a new session, look up or resume any active session for this
worktree/agent and only call Session::open when none exists. Implement a small
discovery step that queries the common_dir/worktree_top for an active session
(e.g. Session::find_active or Session::load_active for the given agent/worktree)
and reuse that Session (preserving its session_id) when found; fall back to
Session::open(...) only if no active session is discovered. Ensure the journal
command uses the discovered Session instance for subsequent logging and for
--final to close the correct session.

In `@crates/tempyr-journal/src/path.rs`:
- Around line 104-107: The function repo_relative_path currently returns early
for non-absolute inputs leaving Windows backslashes intact; update
repo_relative_path so relative paths are normalized the same as absolute ones:
remove the early return, compute the repo-relative path (for absolute inputs
strip the worktree_top prefix, for relative inputs use the input path as-is),
then normalize path separators by replacing backslashes with forward slashes (or
use a to_slash equivalent) before returning the String; reference
repo_relative_path to locate the change.

In `@crates/tempyr-journal/src/session.rs`:
- Around line 182-203: The meta sidecar write is racy because create_new reports
AlreadyExists while the winner may still be writing, causing partial-JSON reads;
fix by writing the JSON to a temporary file next to meta_path (e.g., meta_path +
".tmp"), fsync the temp file, then atomically rename (fs::rename) it to
meta_path so readers never see partial content; additionally, in the
AlreadyExists branch (where you read meta_path into bytes and call
serde_json::from_slice into meta), wrap the read/deserialize in a short retry
loop (with a small sleep/backoff) that retries until deserialization succeeds or
a timeout is reached to handle the rare window before the atomic rename
completes — reference symbols: meta_path, meta, session_id, OpenOptions (current
use), and resume()/the AlreadyExists branch.

In `@crates/tempyr-mcp/src/handler.rs`:
- Around line 678-686: The function relative_cwd currently falls back to
returning an absolute cwd string when cwd is outside worktree_top; change this
so it returns None instead (matching CLI behavior) when strip_prefix fails.
Update the relative_cwd function to only map the strip_prefix result to a
normalized forward-slash string and remove the or_else fallback that returns
cwd.to_string_lossy(); ensure the function returns Option<String> with None for
out-of-repo paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3c5b42b5-dec5-44b5-b93e-0597d48da044

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbb180 and 3ef343d.

📒 Files selected for processing (5)
  • crates/tempyr-cli/src/commands/journal_cmd.rs
  • crates/tempyr-journal/src/kind.rs
  • crates/tempyr-journal/src/path.rs
  • crates/tempyr-journal/src/session.rs
  • crates/tempyr-mcp/src/handler.rs

Comment thread crates/tempyr-cli/src/commands/journal_cmd.rs Outdated
Comment thread crates/tempyr-journal/src/path.rs Outdated
Comment thread crates/tempyr-journal/src/session.rs Outdated
Comment thread crates/tempyr-mcp/src/handler.rs Outdated
CodeRabbit flagged 4 more inline issues; verified each and applied fixes
plus tests:

- session.rs: add `Session::find_active(common_dir, worktree_top, agent)`
  + `Session::open_or_resume(...)` so back-to-back CLI invocations land in
  one session instead of spawning a new timestamped session per call.
  CLI and MCP both switch from `Session::open` to `open_or_resume`. Tests
  cover empty-journals, agent filtering, finalized skip, newest-first
  ordering, and reuse-then-fallback semantics.
- path.rs: `repo_relative_path` now normalizes backslashes to forward
  slashes for *all* inputs (relative ones too) — Windows-style relative
  paths from `--file crates\foo\bar.rs` no longer survive into JSONL.
  Existing test renamed/updated; new test for the relative case.
- session.rs: replace the create_new + read-on-AlreadyExists dance with
  an atomic-write pattern: write JSON to a unique `<id>.meta.json.tmp.<n>`
  via create_new, fsync, then `hard_link` it into place. hard_link fails
  cleanly with AlreadyExists if a concurrent opener won the race (rename
  would silently replace), and `read_meta_with_retry` absorbs the
  microsecond window where the loser sees the link mid-publish.
- handler.rs: drop the `or_else(|| Some(cwd.to_string_lossy()))` fallback
  in `relative_cwd` so an MCP server invoked outside the worktree no
  longer leaks an absolute home-dir path that the redactor would block.
  Now matches the CLI's behavior. Three new unit tests.

Smoke-tested: two `tempyr journal log` calls separated by 2s now share
the same `session_id` (find_active works). All 468 tests pass; clippy
and fmt clean.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tempyr-cli/src/commands/journal_cmd.rs`:
- Around line 129-135: The commit sets entry.is_final but never calls
session.finalize(), so --final never actually closes the session; after
successfully appending the final entry (the append(&session, &entry) call) add a
conditional that if args.is_final is true you call session.finalize() and
map/propagate any error into anyhow (similar to the append error mapping) so the
.ready marker is written and the session is closed; place this after
default_redactor().enforce(...) and append(&session, &entry) succeed.

In `@crates/tempyr-journal/src/session.rs`:
- Around line 167-169: SessionId currently only uses date+worktree hash+second
which allows two different agents to generate the same SessionId and collide;
modify the open flow around SessionId::new, find_active and meta_path handling
so that if a collision is detected but the persisted metadata agent differs from
the requesting agent (i.e., the loaded meta indicates agent != requested agent),
treat it as a retry: loop to generate a new SessionId (waiting or spinning until
the clock advances to a new second) and retry find_active/open until either you
get the matching agent/session or a fresh SessionId is created; update the logic
in the functions/methods that call SessionId::new, find_active, and the open()
path to perform this retry-on-agent-mismatch, and add a regression test that
simulates two agents (e.g., "claude" and "codex") opening the same worktree
within the same second to assert the second caller gets a distinct session and
does not append to the first agent's session.

In `@crates/tempyr-mcp/src/handler.rs`:
- Around line 1727-1730: The tool description on the #[tool(name =
"journal_log", ...)] attribute references "journal_search" which doesn't exist;
remove or replace that reference so clients aren't directed to a non-existent
tool (locate the description string inside the journal_log tool attribute and
delete the phrase "for past reasoning, use journal_search" or reword to omit any
call-to-action to journal_search while keeping the existing safe guidance such
as "For curated knowledge, use graph_add_node").
- Around line 1773-1778: The code currently sets entry.is_final and persists the
entry (via default_redactor().enforce and append) but never finalizes the
session, so add logic after append(&session, &entry) to finalize when
entry.is_final is true: call the existing session-finalization routine (create
the session ".ready" marker or invoke the function that writes the ready marker
/ marks the session ready) so the publisher will be triggered and
Session::find_active() will stop resuming this session; ensure the
finalize/ready-create step runs only after successful append and propagates
errors as map_err(|e| e.to_string()) consistent with surrounding error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5aa0a78-cf4b-42c9-a1db-4fb2ff398f50

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef343d and 0f8dbe7.

📒 Files selected for processing (4)
  • crates/tempyr-cli/src/commands/journal_cmd.rs
  • crates/tempyr-journal/src/path.rs
  • crates/tempyr-journal/src/session.rs
  • crates/tempyr-mcp/src/handler.rs

Comment thread crates/tempyr-cli/src/commands/journal_cmd.rs Outdated
Comment thread crates/tempyr-journal/src/session.rs
Comment thread crates/tempyr-mcp/src/handler.rs
Comment thread crates/tempyr-mcp/src/handler.rs Outdated
CodeRabbit flagged 4 more issues; all verified and fixed:

- session.rs: two agents in the same wall-clock second on the same
  worktree previously got the same SessionId and the loser silently
  reused the winner's session under the wrong agent name. Now `open_at`
  returns a new `JournalError::AgentMismatch { existing, requested }`
  variant on collision, and `open()` retries up to 30 times with brief
  sleeps, advancing `Utc::now()` each pass until the second ticks over.
  Same-agent same-second is still a clean reuse. Replaced the previous
  `open_twice_with_same_ts_does_not_clobber_meta` test with three
  tighter cases:
    - same-agent reuse preserves disk and id
    - different-agent strict-form errors with AgentMismatch
    - live `open()` advances past a same-second collision so the two
      agents end up with distinct ids
- journal_cmd.rs (CLI): `--final` set `entry.is_final` but never wrote
  the `.ready` marker, so `find_active` would keep resuming the session
  forever. Now calls `session.finalize()` after a successful append. CLI
  output reports "Session finalized: <id>" and the `--json` payload gets
  a `finalized` field.
- handler.rs (MCP `journal_log`): same finalize bug — `is_final` on the
  entry never closed the session. Same fix; the JSON response now
  carries `finalized: bool`.
- handler.rs: tool description previously pointed clients at
  `journal_search`, which doesn't exist (Phase 3). Replaced with
  graph_add_node-only routing guidance.

Smoke tests:
  $ tempyr journal log outcome "..." --final --json
  → writes `<id>.ready`; the next CLI call gets a fresh session id
    instead of resuming the finalized one.

All 470 tests pass; clippy and fmt clean.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tempyr-cli/src/commands/journal_cmd.rs`:
- Around line 83-170: The run_log implementation duplicates the
transport-independent journal-write pipeline found in handler.rs (e.g.,
Kind::parse_helpful, cwd_rel computation, Entry::for_session, populating entry
fields, default_redactor().enforce, append, and Session::finalize); extract that
shared logic into a new API in tempyr-journal (e.g., a function like
write_entry_or_finalize(session: &Session, args: &LogArgs) -> Result<(Entry,
bool)>) and have both run_log and handler::journal_log call it; move
path-normalization (cwd_rel), entry population (all entry.* assignments),
redaction (default_redactor().enforce), append(entry), and finalize(session)
into that helper so Session::open_or_resume / Kind::parse_helpful remain caller
responsibilities while transport-specific output (JSON/cli messages) stays in
run_log and handler.rs.

In `@crates/tempyr-mcp/src/handler.rs`:
- Around line 757-761: The code currently hardcodes the agent string "claude"
when opening a journal session via Session::open_or_resume in handler.rs; change
this to use the runtime/server-config agent identity instead (e.g., pull the
agent id from the TempyrServer configuration or MCP runtime context used by this
handler) so the call becomes Session::open_or_resume(common_dir, worktree_top,
agent_id) where agent_id is obtained from the surrounding TempyrServer struct or
handler context (not a string literal), and propagate or validate that agent
value as needed before calling Session::open_or_resume.
- Around line 751-756: The current fast-path returns a cached session when
guard.as_ref() matches common_dir and worktree_top, but it doesn't check whether
that cached Session is already finalized; this can cause reopened journal_log
calls to append to a closed JSONL. Update the conditional around guard.as_ref()
so that after matching cache.common_dir == common_dir && cache.worktree_top ==
worktree_top you also check cache.session.is_ready() (or
!cache.session.is_ready() depending on API) before returning; if the cached
session is finalized call Session::find_active() (reopen) and replace the
cache.session with the newly opened session, then return that active session.
Ensure you reference the same guard/cache variables and maintain existing
cloning semantics for cache.session.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e1d66b7-8357-4a6f-85c2-564e6b374e65

📥 Commits

Reviewing files that changed from the base of the PR and between 0f8dbe7 and 2a76bd9.

📒 Files selected for processing (4)
  • crates/tempyr-cli/src/commands/journal_cmd.rs
  • crates/tempyr-journal/src/lib.rs
  • crates/tempyr-journal/src/session.rs
  • crates/tempyr-mcp/src/handler.rs

Comment thread crates/tempyr-cli/src/commands/journal_cmd.rs
Comment thread crates/tempyr-mcp/src/handler.rs
Comment thread crates/tempyr-mcp/src/handler.rs
CodeRabbit flagged 3 more issues; all verified and fixed:

- writer.rs: extracted the duplicated CLI/MCP write pipeline into
  `EntryDraft` + `write_entry(session, worktree_top, draft)`. The helper
  owns: Entry::for_session population, file-path normalization (via
  jpath::repo_relative_path), cwd relativization, redaction enforcement,
  append, and conditional Session::finalize. Returns a `WriteOutcome
  { entry, finalized }` for transport-specific output formatting.
  Added 3 direct tests (path/cwd normalization + finalize, no-finalize
  path, redactor blocks pre-append).
- journal_cmd.rs (CLI): switched run_log to populate an EntryDraft and
  call write_entry; deleted the now-redundant inline cwd-relativization,
  redactor invocation, append, and finalize. Output formatting (JSON +
  text) stays here.
- handler.rs (MCP): same refactor; the journal_log tool now builds
  EntryDraft from JournalLogParams and calls write_entry. Deleted the
  local `relative_cwd` helper and its three unit tests — the same logic
  now lives once inside write_entry. Removed unused imports of Entry,
  default_redactor, and append.
- handler.rs: hardcoded "claude" agent string replaced with a new
  `agent_id: String` field on TempyrServer (default
  `DEFAULT_AGENT_ID = "claude"`, configurable via
  `with_agent_id(impl Into<String>)`). The journal_session_or_open path
  passes `&self.agent_id` instead of the literal so non-Claude embedders
  can attribute journals correctly.
- handler.rs: cached journal session is now also discarded when
  `cache.session.is_ready()` — i.e., the previous entry triggered
  `finalize()`. Without this check a follow-up `journal_log` call would
  silently keep appending to a session the publisher had already taken
  ownership of. The cache hit now requires `(common_dir, worktree_top,
  !is_ready)` to match.

Smoke-tested:
  $ tempyr journal log finding "..." --json
  → resumes the active session via open_or_resume, writes through the
    new shared pipeline, prints the same shape as before.

All 470 tests pass; clippy and fmt clean. Net +176 LOC (the EntryDraft
struct + WriteOutcome + 3 new tests on the journal side; CLI and MCP
each lost ~30 lines of duplication).

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tempyr-journal/src/writer.rs`:
- Around line 149-158: The current flow calls append(session, &entry) and
session.finalize() separately, which allows races where a "final" entry is
appended but another append slips in before session.finalize() sets the ready
marker; fix by introducing a session-level critical section that atomically
performs the ready-check, append, and (if entry.is_final) finalize. Implement a
new Session method (e.g., Session::append_with_finalization or
Session::atomic_append) that acquires the session mutex/lock, checks if the
session is already ready (rejecting further writes), performs the append
operation, and then, if entry.is_final, calls session.finalize() before
releasing the lock; replace the two-step append + session.finalize() code paths
(the block around default_redactor()/append()/session.finalize() and the similar
block at lines 175-198) to call this atomic method so the ready-marker and JSONL
append cannot be interleaved by concurrent callers.
- Around line 123-132: entry.files are being repo-normalized before resolving
relative inputs against the draft's cwd, causing relative paths like
"src/lib.rs" to remain relative to the CLI working dir; change the mapping that
sets entry.files (using draft.files and jpath::repo_relative_path) to first
resolve each file against the draft.cwd (use the same relative_cwd(draft.cwd,
worktree_top) logic or the already computed entry.cwd) when the file is
relative, then call jpath::repo_relative_path on the resulting absolute or
cwd-resolved path so repo-normalization yields repo-root-relative paths; keep
using draft.files, jpath::repo_relative_path, draft.cwd, relative_cwd and
worktree_top to locate the correct spots to change.

In `@crates/tempyr-mcp/src/handler.rs`:
- Around line 685-693: The cached journal session key currently ignores agent
identity, so Session::open_or_resume() can return a Session tied to the wrong
agent when TempyrServer::with_agent_id() is used or when instances are cloned;
update the cache key and cache-access logic to include agent_id (the agent
string used by Session::open_or_resume()) so JournalSessionCache is keyed by
(common_dir, worktree_top, agent_id), and adjust all places that construct or
lookup journal_session (the journal_session:
Arc<Mutex<Option<JournalSessionCache>>> field and its accessors/fast-path
checks) to use the new triplet key when creating, matching, or resuming sessions
to ensure per-agent isolation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 65d33bc3-7991-488c-ad7a-7a22594692f0

📥 Commits

Reviewing files that changed from the base of the PR and between 2a76bd9 and b0579c2.

📒 Files selected for processing (4)
  • crates/tempyr-cli/src/commands/journal_cmd.rs
  • crates/tempyr-journal/src/lib.rs
  • crates/tempyr-journal/src/writer.rs
  • crates/tempyr-mcp/src/handler.rs

Comment thread crates/tempyr-journal/src/writer.rs Outdated
Comment thread crates/tempyr-journal/src/writer.rs
Comment thread crates/tempyr-mcp/src/handler.rs Outdated
CodeRabbit flagged 3 more issues; all verified and fixed:

- writer.rs: `append(session, entry)` is now atomic with respect to
  session finalization. Under the JSONL exclusive lock it (1) checks
  `session.is_ready()` and refuses with `JournalError::InvalidEntry` if
  the session was finalized between caller's open and this write,
  (2) writes one full line and fsyncs, (3) on `entry.is_final` calls
  `session.finalize()` to drop the `.ready` marker — all before the
  lock is released. Prevents two real races:
    - a concurrent writer slipping an append in between the final
      entry and the marker write
    - a stale CLI/MCP cache writing to a session the publisher has
      already taken ownership of
  Added two tests: post-final straggler is refused (and the JSONL only
  has the two valid entries), and `append_validated` (the publisher
  escape hatch) still works on finalized sessions because it's the
  documented bypass. write_entry's separate `session.finalize()` call
  is gone — append handles it.
- path.rs: new `resolve_file_path(path, worktree_top, cwd)` joins
  relative inputs onto `cwd` before normalizing. `tempyr journal log
  --file src/lib.rs` from `<repo>/crates/foo/` now records as
  `crates/foo/src/lib.rs` instead of `src/lib.rs` (the previous
  behavior would have made the path ambiguous to a reader).
  Absolute inputs and the no-cwd fallback both delegate to
  `repo_relative_path`. write_entry switched to this helper.
  Three new tests; updated the existing write_entry normalization test
  to express the realistic CLI scenario.
- handler.rs: cache key now includes `agent_id` — the field is
  `JournalSessionCache { common_dir, worktree_top, agent_id, session }`
  and the fast-path `if let Some(cache) = guard.as_ref()` check
  requires `cache.agent_id == self.agent_id` along with the existing
  predicates. This matters because `TempyrServer` is `Clone` and
  `with_agent_id(...)` is a builder that consumes `self`: a clone with
  a different agent_id shares the `Arc<Mutex<...>>` cache, and without
  the agent_id key one server's session would silently leak into the
  other.

Smoke-tested:
  $ cd crates/tempyr-journal && tempyr journal log finding "..." \
        --file src/path.rs --json
  → entry.files = ["crates/tempyr-journal/src/path.rs"]
    entry.cwd  = "crates/tempyr-journal"

All 475 tests pass; clippy and fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant