Skip to content

Add runtime trajectory artifacts for session lineage export#1096

Closed
chumyin wants to merge 4 commits into
eastreams:devfrom
chumyin:feat/trajectory-contract-v1-20260408
Closed

Add runtime trajectory artifacts for session lineage export#1096
chumyin wants to merge 4 commits into
eastreams:devfrom
chumyin:feat/trajectory-contract-v1-20260408

Conversation

@chumyin
Copy link
Copy Markdown
Collaborator

@chumyin chumyin commented Apr 8, 2026

Summary

  • Problem:
    • LoongClaw persisted turns, session events, terminal outcomes, approvals, and delegate lineage, but operators still had no single governed artifact to export that evidence for replay, evaluation, or future learning loops.
  • Why it matters:
    • Without one stable trajectory contract, deeper replay/eval work stays blocked on ad hoc sqlite inspection and fragmented local surfaces.
  • What changed:
    • Added an app-layer session::trajectory export contract that assembles full persisted turn history, canonical turn classification, session events, terminal outcomes, and approval requests.
    • Added missing lower-level persistence seams for full turn-record export and direct terminal-outcome upserts.
    • Added a daemon-side runtime-trajectory command family with export and show subcommands plus integration coverage and a public product spec.
    • Hardened env-sensitive verification seams (migrate_cli, Discord channel-status test coverage, ACPX MCP-proxy tests) so repo-wide default and all-features verification is deterministic on this branch.
  • What did not change (scope boundary):
    • No replay executor, no mutation surface, no training loop, no new storage backend, and no change to existing runtime persistence semantics.

Linked Issues

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Security hardening
  • CI / workflow / release

Touched Areas

  • Kernel / policy / approvals
  • Contracts / protocol / spec
  • Daemon / CLI / install
  • Providers / routing
  • Tools
  • Browser automation
  • Channels / integrations
  • ACP / conversation / session runtime
  • Memory / context assembly
  • Config / migration / onboarding
  • Docs / contributor workflow
  • CI / release / workflows

Risk Track

  • Track A (routine / low-risk)
  • Track B (higher-risk / policy-impacting)

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --locked
  • cargo test --workspace --all-features --locked
  • Relevant architecture / dep-graph / docs checks for touched areas
  • Additional scenario, benchmark, or manual checks when behavior changed
  • If this changes config/env fallback, limits, or defaults: include before/after behavior and regression coverage for explicit path, fallback path, and boundary values
  • If tests mutate process-global env: document how state is restored or serialized

Commands and evidence:

cargo fmt --all -- --check
  PASS

cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  PASS

cargo test -p loongclaw-daemon runtime_trajectory --all-features --locked -- --test-threads=1
  PASS

cargo test -p loongclaw-app channel::registry::tests::discord_status_splits_config_backed_send_and_stub_serve --locked -- --test-threads=1
  PASS

cargo test -p loongclaw-daemon migrate_cli_ux_ --locked -- --test-threads=1
  PASS

cargo test -p loongclaw-app acp::acpx::tests::runtime_backend_uses_agent_proxy_when_mcp_servers_requested --all-features --locked -- --exact --test-threads=1
  PASS

cargo test --workspace --locked
  PASS

cargo test --workspace --all-features --locked
  PASS

./scripts/check_architecture_boundaries.sh
./scripts/check_dep_graph.sh
  PASS

Process-global env / PATH stability:

  • The Discord channel-status regression test now clears DISCORD_BOT_TOKEN under ScopedEnv so it no longer depends on ambient local credentials.
  • The ACPX MCP-proxy tests now acquire the shared ScopedEnv lock before probing command/runtime behavior so they do not race other tests that temporarily mutate PATH or related process-global environment.
  • migrate_cli now validates required flags before loading config, so CLI-level UX errors are deterministic even when a local default config is stale or malformed.

User-visible / Operator-visible Changes

  • Added loongclaw runtime-trajectory export
  • Added loongclaw runtime-trajectory show
  • Added a public runtime trajectory artifact contract for session-only and lineage-root-tree export modes
  • Improved CLI verification determinism for migrate/runtime/channel test surfaces

Failure Recovery

  • Fast rollback or disable path:
    • Revert the runtime-trajectory command family and the new app-layer export module; existing persisted session data remains untouched.
    • Revert the verification-hardening follow-up commit if only the hermetic-test adjustments need backing out.
  • Observable failure symptoms reviewers should watch for:
    • Missing lineage root metadata in session-only exports
    • Missing canonical conversation-event or tool-intent rollups in rendered text or JSON artifacts
    • Divergence between exported turn history and approval/session-event counts
    • CLI migrate commands surfacing config parse errors before missing required flag errors

Reviewer Focus

  • crates/app/src/session/trajectory.rs
    • export contract shape, lineage-root resolution, and aggregate rollups
  • crates/app/src/memory/sqlite.rs
    • full persisted turn-record read helper and ordering
  • crates/app/src/session/repository.rs
    • new read-only/full-fidelity session event and terminal-outcome seams
  • crates/daemon/src/runtime_trajectory_cli.rs
    • CLI wiring, artifact round-trip, and text rendering
  • crates/daemon/tests/integration/runtime_trajectory_cli.rs
    • happy-path coverage for session-only export, lineage export, and show/render behavior
  • crates/daemon/src/migrate_cli.rs, crates/daemon/tests/integration/migrate_cli.rs, crates/app/src/channel/registry.rs, crates/app/src/acp/acpx.rs
    • verification determinism fixes and process-global env / PATH isolation

chumyin added 2 commits March 29, 2026 20:41
LoongClaw already persisted turns, session events, terminal outcomes,
approval requests, and delegate lineage, but operators still lacked one
stable artifact that assembled those records into a trajectory export.

This change adds an app-layer runtime trajectory contract, exposes the
missing persisted turn and terminal-outcome seams needed to build it,
and wires a daemon-side `runtime-trajectory` CLI for export/show.
Integration coverage now exercises session-only export, lineage export,
artifact round-trip, and text rendering.

Constraint: Reuse the existing sqlite/session persistence model instead of introducing a new storage backend or daemon-local schema knowledge
Rejected: Implement raw sqlite queries only in daemon | would bypass reusable app-layer seams and harden storage coupling in the wrong layer
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep `runtime-trajectory` read-only until replay or learning workflows have their own explicit governance and policy surfaces
Tested: cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features --locked -- -D warnings; cargo test -p loongclaw-daemon runtime_trajectory --all-features --locked -- --test-threads=1; ./scripts/check_architecture_boundaries.sh; ./scripts/check_dep_graph.sh
Tested: cargo test --workspace --locked (reproduces pre-existing failure in `channel::registry::tests::discord_status_splits_config_backed_send_and_stub_serve`)
Tested: cargo test --workspace --all-features --locked (reproduces the same pre-existing `discord_status_splits_config_backed_send_and_stub_serve` failure)
Not-tested: A completely green full-workspace test run after the unrelated pre-existing Discord channel registry failure is fixed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds workspace tracing deps and observability helpers; instruments core flows with structured tracing; implements a runtime trajectory export feature (session-lineage export, artifact schema, canonicalization, stats) and a daemon CLI (runtime-trajectory export/show) with integration tests and docs.

Changes

Cohort / File(s) Summary
Workspace deps
Cargo.toml, crates/app/Cargo.toml, crates/daemon/Cargo.toml
Added tracing and tracing-subscriber to workspace deps and enabled tracing.workspace = true for crates/app.
Observability modules
crates/app/src/observability.rs, crates/daemon/src/observability.rs
New helpers: JSON key/error summarizers and daemon tracing init (env filter, format selection) with tests.
Tracing instrumentation
crates/app/src/acp/manager.rs, crates/app/src/channel/mod.rs, crates/app/src/tools/mod.rs, crates/app/src/provider/request_failover_runtime.rs, crates/app/src/provider/request_session_runtime.rs
Added structured tracing logs and timing around ACP lifecycle, inbound channel processing, tool execution, and provider failover/session prep.
Memory / SQLite APIs
crates/app/src/memory/sqlite.rs, crates/app/src/memory/mod.rs
New persistable model PersistedConversationTurnRecord, SQL to read all turns for a session, and public direct API session_turn_records_direct (gated by memory-sqlite).
Session repository & hooks
crates/app/src/session/repository.rs, crates/app/src/session/mod.rs
Added list_all_events, upsert_session_terminal_outcome, ALL_EVENTS_PAGE_LIMIT, and exported trajectory module (gated by memory-sqlite).
Runtime trajectory export
crates/app/src/session/trajectory.rs
New export implementation: schema, export modes, canonicalization, lineage resolution, session materialization, stats, and export_runtime_trajectory.
Daemon CLI & wiring
crates/daemon/src/runtime_trajectory_cli.rs, crates/daemon/src/lib.rs, crates/daemon/src/main.rs
New runtime-trajectory CLI subcommands (export, show), CLI wiring, tracing init in main, artifact I/O and text rendering.
Daemon observability
crates/daemon/src/observability.rs
Daemon tracing init and tests; init_tracing() exported and used in main.
Tests
crates/daemon/tests/integration/runtime_trajectory_cli.rs, crates/daemon/tests/integration/cli_tests.rs, crates/daemon/tests/integration/mod.rs
Integration tests for CLI parsing, export/show behavior, artifact contents, ordering, and statistics.
Misc exports & docs
crates/app/src/lib.rs, crates/app/src/provider/runtime_binding.rs, crates/app/src/memory/mod.rs, docs/product-specs/*
Registered observability module, added ProviderRuntimeBinding::as_str(), re-exported new types, and added runtime-trajectory spec/docs.
Other UX checks & tests
crates/daemon/src/migrate_cli.rs, crates/daemon/tests/integration/migrate_cli.rs, crates/app/src/channel/registry.rs
Added migrate CLI flag preflight validation and tests; isolated env usage in channel registry test.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Runtime Trajectory CLI
    participant Export as export_runtime_trajectory()
    participant Repo as SessionRepository
    participant SQLite as Memory/SQLite
    participant Canon as Canonicalizer
    participant Output as Artifact Writer

    CLI->>Export: request export(session_id, mode, config)
    Export->>Repo: load_session_summary(requested_session_id)
    Repo-->>Export: SessionSummary
    Export->>Repo: list_all_events(session_id)
    Repo-->>Export: Vec<SessionEventRecord>
    Export->>SQLite: session_turn_records_direct(session_id)
    SQLite-->>Export: Vec<PersistedConversationTurnRecord}
    Export->>Canon: canonicalize_persisted_turns(...)
    Canon-->>Export: Vec<RuntimeTrajectoryCanonicalRecord>
    Export->>Export: compute_statistics()
    Export->>Output: produce RuntimeTrajectoryArtifactDocument
    Output-->>CLI: JSON or rendered text
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #1089 — Directly implements the runtime-trajectory export/show feature requested by the issue.
  • #902 — Implements trajectory export/inspect surfaces and CLI behavior described by the issue.

Possibly related PRs

  • #905 — Overlaps on trajectory export implementation and CLI surfaces.
  • #718 — Overlaps on observability/tracing additions and instrumentation across app/daemon files.
  • #812 — Overlaps on workspace tracing deps and tracing-subscriber addition and usage.

Suggested labels

docs

Suggested reviewers

  • gh-xj
  • Ari4ka

Poem

🐰 I hopped through logs and sqlite rows,

Turn by turn the trajectory grows,
JSON bundled, tidy and bright,
CLI in paw, I print the sight,
A rabbit cheers: "Export's just right!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and specifically summarizes the main change: adding runtime trajectory artifacts for session lineage export, which aligns with the primary objective of the PR.
Linked Issues check ✅ Passed All acceptance criteria from issue #1089 are met: stable JSON artifact schema defined, session-only and lineage modes supported, full turn history and canonical records included, round-trip via show command implemented, and integration tests verify lineage export and artifact handling.
Out of Scope Changes check ✅ Passed All changes align with stated objectives. Minor supporting changes (observability helpers, migrate CLI validation, Discord test isolation) are necessary for the core feature and do not introduce unrelated functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation. dependencies Pull requests that update dependency files. spec Architecture boundaries, product specs, and design docs. daemon Daemon binary, CLI entrypoints, and install flow. providers Provider routing, selection, and transport behavior. tools Tool runtime, policy adapters, and tool catalog behavior. channels Channel adapters and external integration surfaces. memory Memory system, context assembly, and persistence flow. conversation Conversation runtime, session flow, and prompt assembly. acp ACP manager, binding, routing, and control plane surfaces. size: XL Very large pull request: more than 1000 changed lines. labels Apr 8, 2026
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

🧹 Nitpick comments (2)
crates/app/src/session/repository.rs (1)

1508-1544: Two similar upsert methods with subtle differences.

This new upsert_session_terminal_outcome uses load_session_summary_with_legacy_fallback (supporting legacy sessions inferred from turn history), while the existing upsert_terminal_outcome at line 1249 uses load_session (requiring explicit session rows).

The naming distinction (session_ prefix) is helpful, but consider adding a brief doc comment clarifying when to use each method to prevent confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/src/session/repository.rs` around lines 1508 - 1544, Add a short
doc comment above upsert_session_terminal_outcome that explains it validates
existence via load_session_summary_with_legacy_fallback (which allows legacy
sessions inferred from turn history), and contrast it with
upsert_terminal_outcome which uses load_session (requiring an explicit session
row); mention intended usage scenarios and when to prefer each function so
callers won't be confused by the similar names.
crates/app/src/provider/runtime_binding.rs (1)

38-46: Test covers only the Direct variant.

The stability test validates Direct but not the Kernel(_) variant. If label stability matters for serialization, consider adding coverage for the Kernel case in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/src/provider/runtime_binding.rs` around lines 38 - 46, The test
only asserts the label for ProviderRuntimeBinding::direct(); add a corresponding
assertion for the Kernel variant to ensure label stability by constructing a
Kernel variant (e.g., ProviderRuntimeBinding::kernel("...") or
ProviderRuntimeBinding::Kernel(some_name) depending on the enum constructor) and
calling its as_str() to assert the expected stable label; update or add a test
in the tests module (near provider_runtime_binding_labels_are_stable) to include
ProviderRuntimeBinding::Kernel(...) and assert_eq!(... , "kernel") or the exact
expected string returned by as_str().
🤖 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/app/src/memory/sqlite.rs`:
- Around line 665-677: The validation error message used when normalizing
session_id is misleading for the direct API; update the call to
normalize_required_str in session_turn_records_direct to use a message referring
to the direct parameter (e.g., "memory.session_turn_records_direct requires
session_id" or similar) instead of "payload.session_id" so callers see correct
wording; locate the normalize_required_str invocation in
session_turn_records_direct and change only the error string to direct-API
phrasing.

In `@crates/app/src/session/trajectory.rs`:
- Around line 161-171: The export assembly opens multiple independent DB
connections (calls via SessionRepository::new then
load_session_summary_with_legacy_fallback, resolve_root_session_id,
collect_export_session_summaries, collect_export_sessions,
build_runtime_trajectory_statistics) which can produce torn/ inconsistent
artifacts; wrap the entire sequence that builds requested_summary,
root_session_id, session_summaries, sessions and statistics in a single
read-only transaction (or obtain a single connection/transaction from
SessionRepository and pass it into those collection functions) so all reads
occur under one consistent snapshot (similar to replace_turns_internal with
TransactionBehavior::Immediate).

In `@crates/daemon/src/main.rs`:
- Line 42: The debug log currently emits the full parsed CLI payload via
tracing::debug!(..., command = ?cli.command, ...), which may leak user-provided
content; change the log to avoid serializing the entire cli.command payload and
instead emit only non-sensitive metadata — e.g., whether a subcommand exists or
the command variant name. Locate the tracing::debug! call that references
cli.command and replace the argument with a safe value such as a boolean like
cli.command.is_some() or the command variant identifier (derive it by
pattern-matching cli.command to extract only the variant name or use
std::mem::discriminant) so no user data is logged.

In `@crates/daemon/src/runtime_trajectory_cli.rs`:
- Around line 161-176: The deserialized artifact in
load_runtime_trajectory_artifact should be validated for schema.version,
surface, and purpose before returning to enforce the versioned contract; after
calling serde_json::from_str in load_runtime_trajectory_artifact, check that
artifact.schema.version == "<expected_version>" (or matches allowed versions),
artifact.schema.surface == "<expected_surface>" and artifact.schema.purpose ==
"<expected_purpose>" and return a CliResult::Err (using the same error format
used elsewhere) if any check fails, including the offending values and the
path.display() in the error message so incompatible or foreign artifacts are
rejected instead of silently accepted.

---

Nitpick comments:
In `@crates/app/src/provider/runtime_binding.rs`:
- Around line 38-46: The test only asserts the label for
ProviderRuntimeBinding::direct(); add a corresponding assertion for the Kernel
variant to ensure label stability by constructing a Kernel variant (e.g.,
ProviderRuntimeBinding::kernel("...") or
ProviderRuntimeBinding::Kernel(some_name) depending on the enum constructor) and
calling its as_str() to assert the expected stable label; update or add a test
in the tests module (near provider_runtime_binding_labels_are_stable) to include
ProviderRuntimeBinding::Kernel(...) and assert_eq!(... , "kernel") or the exact
expected string returned by as_str().

In `@crates/app/src/session/repository.rs`:
- Around line 1508-1544: Add a short doc comment above
upsert_session_terminal_outcome that explains it validates existence via
load_session_summary_with_legacy_fallback (which allows legacy sessions inferred
from turn history), and contrast it with upsert_terminal_outcome which uses
load_session (requiring an explicit session row); mention intended usage
scenarios and when to prefer each function so callers won't be confused by the
similar names.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5e1c0db7-5ccb-4f22-bdb1-a4afbf682fa7

📥 Commits

Reviewing files that changed from the base of the PR and between 0d6bb6c and 0535391.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • Cargo.toml
  • crates/app/Cargo.toml
  • crates/app/src/acp/manager.rs
  • crates/app/src/channel/mod.rs
  • crates/app/src/lib.rs
  • crates/app/src/memory/mod.rs
  • crates/app/src/memory/sqlite.rs
  • crates/app/src/observability.rs
  • crates/app/src/provider/request_failover_runtime.rs
  • crates/app/src/provider/request_session_runtime.rs
  • crates/app/src/provider/runtime_binding.rs
  • crates/app/src/session/mod.rs
  • crates/app/src/session/repository.rs
  • crates/app/src/session/trajectory.rs
  • crates/app/src/tools/mod.rs
  • crates/daemon/Cargo.toml
  • crates/daemon/src/lib.rs
  • crates/daemon/src/main.rs
  • crates/daemon/src/observability.rs
  • crates/daemon/src/runtime_trajectory_cli.rs
  • crates/daemon/tests/integration/cli_tests.rs
  • crates/daemon/tests/integration/mod.rs
  • crates/daemon/tests/integration/runtime_trajectory_cli.rs
  • docs/product-specs/index.md
  • docs/product-specs/runtime-trajectory.md

Comment on lines +665 to +677
pub(super) fn session_turn_records_direct(
session_id: &str,
config: &MemoryRuntimeConfig,
) -> Result<Vec<PersistedConversationTurnRecord>, String> {
let session_id = normalize_required_str(
session_id,
"memory.session_turn_records requires payload.session_id",
)?;
let runtime = acquire_memory_runtime(config)?;
runtime.with_connection("memory.session_turn_records", |conn| {
query_all_turn_records(conn, session_id)
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use direct-API wording in the validation error message.

At Line 671, the error says payload.session_id, but this function accepts a direct session_id: &str. That can surface misleading errors to callers.

✏️ Proposed fix
 pub(super) fn session_turn_records_direct(
     session_id: &str,
     config: &MemoryRuntimeConfig,
 ) -> Result<Vec<PersistedConversationTurnRecord>, String> {
     let session_id = normalize_required_str(
         session_id,
-        "memory.session_turn_records requires payload.session_id",
+        "memory.session_turn_records requires session_id",
     )?;
     let runtime = acquire_memory_runtime(config)?;
     runtime.with_connection("memory.session_turn_records", |conn| {
         query_all_turn_records(conn, session_id)
     })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/src/memory/sqlite.rs` around lines 665 - 677, The validation error
message used when normalizing session_id is misleading for the direct API;
update the call to normalize_required_str in session_turn_records_direct to use
a message referring to the direct parameter (e.g.,
"memory.session_turn_records_direct requires session_id" or similar) instead of
"payload.session_id" so callers see correct wording; locate the
normalize_required_str invocation in session_turn_records_direct and change only
the error string to direct-API phrasing.

Comment on lines +161 to +171
let repo = SessionRepository::new(memory_config)?;
let requested_summary = repo
.load_session_summary_with_legacy_fallback(requested_session_id.as_str())?
.ok_or_else(|| {
format!("runtime trajectory export session `{requested_session_id}` was not found")
})?;
let root_session_id = resolve_root_session_id(&repo, requested_session_id.as_str())?;
let session_summaries =
collect_export_session_summaries(&repo, &requested_summary, &root_session_id, export_mode)?;
let sessions = collect_export_sessions(&repo, memory_config, session_summaries.as_slice())?;
let statistics = build_runtime_trajectory_statistics(sessions.as_slice());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C4 '\bfn (session_turn_records_direct|list_visible_sessions|session_lineage_depth|list_all_events|load_terminal_outcome|list_approval_requests_for_session)\b' \
  crates/app/src/memory/sqlite.rs crates/app/src/session/repository.rs

rg -n -C4 'TransactionBehavior|transaction\(|unchecked_transaction\(' \
  crates/app/src/memory/sqlite.rs crates/app/src/session/repository.rs

Repository: loongclaw-ai/loongclaw

Length of output: 11884


🏁 Script executed:

# First, let's look at the export_runtime_trajectory function and surrounding code
sed -n '140,200p' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 2395


🏁 Script executed:

# Check the collect_export_sessions implementation
rg -n -A20 'fn collect_export_sessions' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 1012


🏁 Script executed:

# Check SessionRepository.open_connection to see if each call opens a fresh connection
rg -n -A10 'fn open_connection' crates/app/src/session/repository.rs

Repository: loongclaw-ai/loongclaw

Length of output: 670


🏁 Script executed:

# Look for any transaction wrapping in the export path
rg -n -B5 -A15 'fn export_runtime_trajectory' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 906


🏁 Script executed:

# Check if collect_export_sessions opens its own connections or reuses a transaction
rg -n -B3 -A20 'fn collect_export_sessions' crates/app/src/session/

Repository: loongclaw-ai/loongclaw

Length of output: 1926


🏁 Script executed:

sed -n '262,295p' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 1306


🏁 Script executed:

# Also check if there are any existing transaction patterns in the export module
rg -n 'transaction|Transaction' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 48


🏁 Script executed:

# Verify session_turn_records_direct opens its own connection
sed -n '665,700p' crates/app/src/memory/sqlite.rs

Repository: loongclaw-ai/loongclaw

Length of output: 1610


🏁 Script executed:

# Check if there's any transaction-based access pattern in repository methods
rg -n 'with_connection|open_connection' crates/app/src/session/repository.rs | head -20

Repository: loongclaw-ai/loongclaw

Length of output: 1041


🏁 Script executed:

# Verify the exact lines 161-171 match the review snippet
sed -n '161,171p' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 765


🏁 Script executed:

# Check if there's a transaction wrapper around export or any snapshot mechanism
rg -n -B10 'export_runtime_trajectory' crates/app/src/session/trajectory.rs | head -30

Repository: loongclaw-ai/loongclaw

Length of output: 490


🏁 Script executed:

# Verify what happens between reading summary and reading related data
rg -n 'lineage_root_session_id|session_lineage_depth|list_visible_sessions' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 353


🏁 Script executed:

# Check what SessionRepository.new does - does it create a shared connection or factory?
rg -n -A15 'impl SessionRepository' crates/app/src/session/repository.rs | head -40

Repository: loongclaw-ai/loongclaw

Length of output: 853


🏁 Script executed:

# Verify SessionRepository::new implementation
rg -n -A10 'fn new(' crates/app/src/session/repository.rs | grep -A10 'SessionRepository'

Repository: loongclaw-ai/loongclaw

Length of output: 130


🏁 Script executed:

# Check if there's any concurrent write activity that could interleave with export
rg -n 'append_turn|replace_turns|create_session' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 48


🏁 Script executed:

# Let's check if there's any snapshot or transaction wrapping at a higher level
rg -n -B5 -A5 'export_runtime_trajectory' crates/app/src/ --type rust | head -50

Repository: loongclaw-ai/loongclaw

Length of output: 900


🏁 Script executed:

# Check if there's any mechanism to coordinate multi-statement reads
rg -n 'with_connection_mut|transaction_with_behavior' crates/app/src/session/repository.rs

Repository: loongclaw-ai/loongclaw

Length of output: 140


🏁 Script executed:

# Verify the actual isolation behavior - check if there's any READ UNCOMMITTED or snapshot isolation
rg -n 'PRAGMA|isolation' crates/app/src/memory/sqlite.rs

Repository: loongclaw-ai/loongclaw

Length of output: 384


🏁 Script executed:

# Check if SessionRepository methods use any shared connection or transaction context
rg -n 'fn list_visible_sessions' -A30 crates/app/src/session/repository.rs | head -40

Repository: loongclaw-ai/loongclaw

Length of output: 1890


🏁 Script executed:

# Verify if load_session_summary_with_legacy_fallback uses a transaction
rg -n 'fn load_session_summary_with_legacy_fallback' -A20 crates/app/src/session/repository.rs

Repository: loongclaw-ai/loongclaw

Length of output: 1927


🏁 Script executed:

# Check what isolation level SQLite is using
rg -n 'journal_mode|IMMEDIATE|DEFERRED|EXCLUSIVE' crates/app/src/

Repository: loongclaw-ai/loongclaw

Length of output: 336


🏁 Script executed:

# Check if any recent changes to export_runtime_trajectory have addressed the transaction issue
git log --oneline -20 -- crates/app/src/session/trajectory.rs 2>/dev/null || echo "git log not available"

Repository: loongclaw-ai/loongclaw

Length of output: 139


🏁 Script executed:

# Verify if there's a pattern elsewhere in the codebase for multi-step operations that needs atomicity
rg -n 'fn.*\(' crates/app/src/session/trajectory.rs | grep -E 'export|collect'

Repository: loongclaw-ai/loongclaw

Length of output: 176


🏁 Script executed:

# Check if SQLite WAL mode provides sufficient isolation for the concurrent scenario
rg -n 'synchronous|wal_autocheckpoint' crates/app/src/memory/sqlite.rs

Repository: loongclaw-ai/loongclaw

Length of output: 214


🏁 Script executed:

# Check the recent commit that mentions "export"
git show 0535391 --stat 2>/dev/null | head -20 || echo "Cannot show commit details"

Repository: loongclaw-ai/loongclaw

Length of output: 1180


🏁 Script executed:

# Look for any subsequent fixes after the export was added
git log --oneline -30 -- crates/app/src/session/trajectory.rs 2>/dev/null | head -20

Repository: loongclaw-ai/loongclaw

Length of output: 139


🏁 Script executed:

# Verify if WAL mode with NORMAL synchronous provides sufficient isolation
rg -n 'PRAGMA|isolation_level|wal' crates/app/src/memory/sqlite.rs | head -15

Repository: loongclaw-ai/loongclaw

Length of output: 384


🏁 Script executed:

# Check if the export functions accept or pass a transaction context
rg -n 'Transaction|&tx' crates/app/src/session/trajectory.rs

Repository: loongclaw-ai/loongclaw

Length of output: 48


Wrap the export assembly in a single transaction to ensure a consistent artifact snapshot.

The export performs multiple independent persistence reads—metadata, turns, events, outcomes, and approval requests—each opening a fresh connection. Without a transaction boundary, a session mutating between reads can cause the artifact to become internally inconsistent (e.g., summary.turn_count mismatch with actual turns, or lineage changes mid-export). WAL mode does not prevent torn reads across separate statements without an explicit transaction. Wrap the entire sequence in a read-only transaction or pass a single connection through all collection steps to match the pattern already used in replace_turns_internal with TransactionBehavior::Immediate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/src/session/trajectory.rs` around lines 161 - 171, The export
assembly opens multiple independent DB connections (calls via
SessionRepository::new then load_session_summary_with_legacy_fallback,
resolve_root_session_id, collect_export_session_summaries,
collect_export_sessions, build_runtime_trajectory_statistics) which can produce
torn/ inconsistent artifacts; wrap the entire sequence that builds
requested_summary, root_session_id, session_summaries, sessions and statistics
in a single read-only transaction (or obtain a single connection/transaction
from SessionRepository and pass it into those collection functions) so all reads
occur under one consistent snapshot (similar to replace_turns_internal with
TransactionBehavior::Immediate).

Comment thread crates/daemon/src/main.rs
let _stdin_guard = StdinGuard;
init_tracing();
let cli = Cli::parse();
tracing::debug!(target: "loongclaw.daemon", command = ?cli.command, "parsed CLI command");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging full parsed CLI payloads.

Logging ?cli.command can capture user-provided content and identifiers in debug logs. Log only non-sensitive metadata (e.g., whether a subcommand was provided).

🔧 Proposed change
-    tracing::debug!(target: "loongclaw.daemon", command = ?cli.command, "parsed CLI command");
+    tracing::debug!(
+        target: "loongclaw.daemon",
+        has_explicit_command = cli.command.is_some(),
+        "parsed CLI command"
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tracing::debug!(target: "loongclaw.daemon", command = ?cli.command, "parsed CLI command");
tracing::debug!(
target: "loongclaw.daemon",
has_explicit_command = cli.command.is_some(),
"parsed CLI command"
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/daemon/src/main.rs` at line 42, The debug log currently emits the full
parsed CLI payload via tracing::debug!(..., command = ?cli.command, ...), which
may leak user-provided content; change the log to avoid serializing the entire
cli.command payload and instead emit only non-sensitive metadata — e.g., whether
a subcommand exists or the command variant name. Locate the tracing::debug! call
that references cli.command and replace the argument with a safe value such as a
boolean like cli.command.is_some() or the command variant identifier (derive it
by pattern-matching cli.command to extract only the variant name or use
std::mem::discriminant) so no user data is logged.

Comment on lines +161 to +176
fn load_runtime_trajectory_artifact(path: &Path) -> CliResult<RuntimeTrajectoryArtifactDocument> {
let raw = fs::read_to_string(path).map_err(|error| {
format!(
"read runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
let artifact =
serde_json::from_str::<RuntimeTrajectoryArtifactDocument>(&raw).map_err(|error| {
format!(
"decode runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
Ok(artifact)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate schema.version/surface/purpose before accepting an artifact.

show currently renders any JSON that deserializes into RuntimeTrajectoryArtifactDocument. That makes the versioned artifact contract unenforced and lets future or foreign artifacts be treated as compatible. ``

Proposed fix
 fn load_runtime_trajectory_artifact(path: &Path) -> CliResult<RuntimeTrajectoryArtifactDocument> {
     let raw = fs::read_to_string(path).map_err(|error| {
         format!(
             "read runtime trajectory artifact {} failed: {error}",
             path.display()
@@
     let artifact =
         serde_json::from_str::<RuntimeTrajectoryArtifactDocument>(&raw).map_err(|error| {
             format!(
                 "decode runtime trajectory artifact {} failed: {error}",
                 path.display()
             )
         })?;
+    if artifact.schema.version
+        != loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_JSON_SCHEMA_VERSION
+        || artifact.schema.surface.as_str()
+            != loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_SURFACE
+        || artifact.schema.purpose.as_str()
+            != loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_PURPOSE
+    {
+        return Err(format!(
+            "runtime trajectory artifact {} has unsupported schema version={} surface={} purpose={}",
+            path.display(),
+            artifact.schema.version,
+            artifact.schema.surface,
+            artifact.schema.purpose
+        ));
+    }
     Ok(artifact)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn load_runtime_trajectory_artifact(path: &Path) -> CliResult<RuntimeTrajectoryArtifactDocument> {
let raw = fs::read_to_string(path).map_err(|error| {
format!(
"read runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
let artifact =
serde_json::from_str::<RuntimeTrajectoryArtifactDocument>(&raw).map_err(|error| {
format!(
"decode runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
Ok(artifact)
}
fn load_runtime_trajectory_artifact(path: &Path) -> CliResult<RuntimeTrajectoryArtifactDocument> {
let raw = fs::read_to_string(path).map_err(|error| {
format!(
"read runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
let artifact =
serde_json::from_str::<RuntimeTrajectoryArtifactDocument>(&raw).map_err(|error| {
format!(
"decode runtime trajectory artifact {} failed: {error}",
path.display()
)
})?;
if artifact.schema.version
!= loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_JSON_SCHEMA_VERSION
|| artifact.schema.surface.as_str()
!= loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_SURFACE
|| artifact.schema.purpose.as_str()
!= loongclaw_app::session::trajectory::RUNTIME_TRAJECTORY_ARTIFACT_PURPOSE
{
return Err(format!(
"runtime trajectory artifact {} has unsupported schema version={} surface={} purpose={}",
path.display(),
artifact.schema.version,
artifact.schema.surface,
artifact.schema.purpose
));
}
Ok(artifact)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/daemon/src/runtime_trajectory_cli.rs` around lines 161 - 176, The
deserialized artifact in load_runtime_trajectory_artifact should be validated
for schema.version, surface, and purpose before returning to enforce the
versioned contract; after calling serde_json::from_str in
load_runtime_trajectory_artifact, check that artifact.schema.version ==
"<expected_version>" (or matches allowed versions), artifact.schema.surface ==
"<expected_surface>" and artifact.schema.purpose == "<expected_purpose>" and
return a CliResult::Err (using the same error format used elsewhere) if any
check fails, including the offending values and the path.display() in the error
message so incompatible or foreign artifacts are rejected instead of silently
accepted.

chumyin added 2 commits April 8, 2026 13:19
…ate flows

The runtime trajectory slice was ready, but full-workspace verification still
wasn't trustworthy because two tests were sensitive to machine-local state:
Discord channel readiness could silently flip when DISCORD_BOT_TOKEN was set,
and migrate CLI UX assertions could be pre-empted by parsing an unrelated
existing config before required flag validation ran.

This follow-up makes the Discord registry assertion hermetic with the existing
ScopedEnv guard and moves migrate required-flag validation ahead of config
loading. It also adds regression coverage for missing input/output behavior
across apply, discover, and rollback modes so future UX regressions fail
before runtime/tool execution begins.

Constraint: Preserve existing runtime behavior and fix only the test/CLI ordering seams that made verification nondeterministic
Rejected: Change Discord channel readiness semantics to ignore configured env defaults | would break legitimate config-backed readiness and hide real operator state
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep CLI-required-flag validation ahead of config loading for UX-critical migrate modes so local config drift cannot mask argument errors
Tested: cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features --locked -- -D warnings; cargo test -p loongclaw-app channel::registry::tests::discord_status_splits_config_backed_send_and_stub_serve --locked -- --test-threads=1; cargo test -p loongclaw-daemon integration::migrate_cli::migrate_cli_ux_apply_mode_reports_flag_level_output_requirement --locked -- --exact --test-threads=1; cargo test --workspace --locked; cargo test --workspace --all-features --locked
Not-tested: none
The runtime trajectory work uncovered one more verification-only problem:
ACPX MCP-proxy tests depended on ambient process environment stability while
other tests were free to mutate environment variables under the shared test
process. That made the all-features test pass non-deterministic even though the
runtime code path itself was correct.

This change serializes the affected ACPX tests behind the existing ScopedEnv
lock so PATH- and runtime-probe-sensitive assertions do not race unrelated
environment-mutating tests.

Constraint: Preserve the production ACPX runtime behavior and only harden the test harness
Rejected: Add more ad-hoc sleeps/retries inside the test body | would hide the shared-environment race instead of removing it
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any ACPX/browser/runtime test that depends on process-global env or PATH should acquire the shared ScopedEnv lock before probing external commands
Tested: cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features --locked -- -D warnings; cargo test -p loongclaw-app acp::acpx::tests::runtime_backend_uses_agent_proxy_when_mcp_servers_requested --all-features --locked -- --exact --test-threads=1; cargo test --workspace --locked; cargo test --workspace --all-features --locked; ./scripts/check_architecture_boundaries.sh; ./scripts/check_dep_graph.sh
Not-tested: No additional non-Unix ACPX coverage because the guarded tests are already `#[cfg(unix)]`
@chumyin chumyin self-assigned this Apr 8, 2026
@gh-xj
Copy link
Copy Markdown
Collaborator

gh-xj commented Apr 8, 2026

Dispatcher review status: CASE-20260408-loongclaw-pr-1096

Status: needs_rebase
Risk: high
Reviewed head: 6e22531b0ac31b05289078f279f66c41e7de6bb1

Reconciled findings on the current head:

  • crates/app/src/session/trajectory.rs: runtime-trajectory export assembles one artifact through multiple independent persistence reads, so a live session can mutate between reads and yield a torn snapshot.
  • crates/daemon/src/runtime_trajectory_cli.rs: runtime-trajectory show accepts any structurally compatible JSON and does not enforce the declared schema version/surface/purpose contract.
  • crates/daemon/src/main.rs: daemon debug logging now serializes the full parsed CLI payload instead of only non-sensitive command metadata.

Executable proof run locally on this head:

  • cargo test -p loongclaw-daemon runtime_trajectory --all-features --locked -- --test-threads=1
  • cargo test -p loongclaw-daemon integration::migrate_cli::migrate_cli_ux_apply_mode_reports_flag_level_output_requirement --locked -- --exact --test-threads=1
  • cargo test -p loongclaw-app channel::registry::tests::discord_status_splits_config_backed_send_and_stub_serve --locked -- --test-threads=1
  • cargo test -p loongclaw-app acp::acpx::tests::runtime_backend_uses_agent_proxy_when_mcp_servers_requested --all-features --locked -- --exact --test-threads=1

External review intake:

  • The current CodeRabbit inline comments were classified as valid and align with the reconciled review.

Blocking conditions before closure:

  • Rebase onto dev (the PR is currently CONFLICTING).
  • Fix the reconciled findings.
  • Re-run the targeted proof set on the new head.

@gh-xj
Copy link
Copy Markdown
Collaborator

gh-xj commented Apr 8, 2026

Dispatcher review for current head 6e22531b0ac31b05289078f279f66c41e7de6bb1 found blocking issues.

  1. crates/app/src/session/trajectory.rs: export_runtime_trajectory assembles summary metadata, turns, events, terminal outcomes, and approval requests through separate reads with no shared snapshot/transaction, so live exports can be internally inconsistent.
  2. crates/daemon/src/runtime_trajectory_cli.rs: runtime-trajectory show accepts foreign artifacts without validating schema.version, schema.surface, or schema.purpose. I manually reproduced this on the current head by feeding a deliberately incompatible artifact and observing a successful render.
  3. crates/daemon/src/main.rs: the new top-level debug log emits the full parsed CLI command payload, which leaks user-provided content whenever debug logging is enabled.

Verification run on the reviewed head:

  • cargo test -p loongclaw-daemon runtime_trajectory --all-features --locked -- --test-threads=1
  • cargo test -p loongclaw-daemon integration::migrate_cli:: --locked -- --test-threads=1
  • cargo test -p loongclaw-app channel::registry::tests::discord_status_splits_config_backed_send_and_stub_serve --locked -- --exact --test-threads=1
  • cargo test -p loongclaw-app acp::acpx::tests::runtime_backend_uses_agent_proxy_when_mcp_servers_requested --all-features --locked -- --exact --test-threads=1

All four current CodeRabbit inline comments were also verified against the live head and classified as valid. This PR is blocked pending fixes for the issues above.

@chumyin
Copy link
Copy Markdown
Collaborator Author

chumyin commented Apr 8, 2026

closing this as superseded by merged #1097. i kept #1097 as the narrower session-scoped export/show slice for #1090, so i don't want to leave a second trajectory-only branch open with the older lineage-oriented scope.

@chumyin chumyin closed this Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

acp ACP manager, binding, routing, and control plane surfaces. channels Channel adapters and external integration surfaces. conversation Conversation runtime, session flow, and prompt assembly. daemon Daemon binary, CLI entrypoints, and install flow. dependencies Pull requests that update dependency files. documentation Improvements or additions to documentation. memory Memory system, context assembly, and persistence flow. providers Provider routing, selection, and transport behavior. size: XL Very large pull request: more than 1000 changed lines. spec Architecture boundaries, product specs, and design docs. tools Tool runtime, policy adapters, and tool catalog behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: add runtime trajectory export for session lineage artifacts

2 participants