Privacy controls, integration test harness, and state-locking fixes#4
Merged
Conversation
- openspec initialised - plan based on discussion with GLM-5.2
Track 2 (cargo test): drives the binary directly with crafted payloads against an in-process fake Langfuse (ingestion/list/delete + test control plane with latency injection), tmpdir-isolated per test. Covers pause/resume round-trip, both purge semantics pins, and the accepted purge-vs-in-flight-send window. Three tests land #[ignore]d red — they demonstrate the non-blocking-flock lost-update bug and are the acceptance criteria for fix-state-locking. Track 1 (harness/): real pinned claude CLI in a container with code-trace wired as SessionStart/Stop hooks, a stub Anthropic Messages API (no key, no cost — see harness/NOTES.md for the spike), and the same fake Langfuse as a standalone bin. Five scenarios verified locally end-to-end; compose stack awaits its first CI run. Also: CODE_TRACE_SYNC_SEND=1 sends inline instead of forking so process exit implies delivery — makes "paused session sent nothing" assertions exact. Production fork path unchanged. Implements openspec change add-integration-harness (20/20 tasks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FileLock::acquire passed LOCK_EX | LOCK_NB and ignored the result, so the lock excluded nobody: concurrent invocations were last-writer-wins on state.json. A pause landing during another session's emit could be silently overwritten — the paused session kept tracing — and concurrent cursor saves could be lost, re-emitting duplicate traces. The lock is now a blocking LOCK_EX with the result checked: contenders queue, EINTR is retried, and genuine flock failure logs loudly before proceeding best-effort. Documented invariants on the type: one acquisition per process (a second blocking acquire on a new fd would self-deadlock), and purge deliberately holds the lock across its Langfuse HTTP calls. The three #[ignore]d red tests from the harness change are un-ignored and now pass: lock mutual exclusion, pause-vs-emit lost update, and the concurrent-emit stress invariants. Implements openspec change fix-state-locking (7/7 tasks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suppression exit ran before the transcript was read, so a paused session's cursor never advanced — pause deferred tracing rather than preventing it. The first emit after resume replayed every paused-period turn to Langfuse: exactly the content the user paused to protect. OpenCode/Pi were unaffected (their plugins offer each message once), so Claude Code's replay was also an inconsistency between sources. The suppressed path now consumes its input: the cursor (offset, buffer, turn count) advances past the paused turns, but no ingestion event is built and no send is forked, for all three sources. Paused turns are permanently untraceable, and turn numbering shows a visible gap where the pause was. Both regression tests flipped red-first: the pause/resume round-trip now asserts [1, 3], and the suppressed-session test asserts the cursor advances while zero bytes reach the server. Implements openspec change fix-paused-turn-replay (4/4 tasks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A user reported that freshly installing code-trace uploaded traces from before it was installed: the first Stop hook for a session with no cursor read the transcript from byte 0 and emitted the entire history. Same failure family as the pause-replay fix — content the user never intended to trace ships because the cursor didn't cover it. Triggers: install mid-session, --resume of a pre-install session, enabling TRACE_TO_LANGFUSE later, or a 7-day-pruned cursor (which previously re-emitted everything as duplicates). On first contact (no cursor entry) the emit path now emits only the turn that fired the hook — which completed under an installed, enabled code-trace — and fast-forwards past everything earlier, counting the skipped turns so numbering shows the gap. Invisible for genuinely new sessions, where the last turn is the only turn. Applies to all three sources. The skip is logged. Documented limitation: with an existing cursor, turns completed while TRACE_TO_LANGFUSE=false still emit on re-enable (the disabled path touches no state by design); pause is the airtight mechanism. Implements openspec change fix-first-contact-backlog (4/4 tasks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Synced delta specs into openspec/specs/ — eleven capabilities created (cli-dispatch, private-mode, session-purge, session-registry, startup-reminder, fake-langfuse, sync-send-mode, claude-seam-tests, concurrency-tests, state-locking, first-contact-cap), with fix-paused-turn-replay's MODIFIED delta applied on top of private-mode (consume-without-emitting semantics). Archived in dependency order: add-privacy-controls, add-integration-harness, fix-state-locking, fix-paused-turn-replay, fix-first-contact-backlog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- purge no longer holds the blocking state lock across its interactive confirmation prompt: the plan is built from a lock-free read (saves are atomic) and the lock is taken only for the mutation, so a human idling at y/N cannot stall every hook on the machine - harness runner waits for fake-langfuse and the stub model to actually answer before the first scenario (compose depends_on only orders container start, not readiness) - fake langfuse stores ingestion events before counting the request, so wait_for_ingestion_posts cannot observe a counted-but-empty POST; guard limit=0 in pagination - widen the hold-test timing margin (1s hold / 500ms ceiling) for slow CI Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds per-session privacy controls to code-trace and the test infrastructure to prove they hold, plus three bug fixes the new tests exposed or motivated.
Privacy controls (
pause/resume/purge/sessions/status/--on-start): per-session tracing suppression persisted in a session registry, purge of all three data copies (Langfuse traces, local transcript, cursor state), and a SessionStart reminder line.Two-track integration harness:
cargo test): crafted payloads against an in-process fake Langfuse, tmpdir-isolated; covers pause/resume/purge semantics and deterministic concurrency scenarios sequenced via the state lock file.harness/): the real pinnedclaudeCLI in a container with code-trace wired as hooks, a stub Messages API (no key, no cost), and the fake Langfuse as a service. All five scenarios verified locally; theharnessCI job in this PR is its first in-Docker run.Fixes, each landed red-test-first:
flockpassedLOCK_NBand ignored the result, so concurrent invocations were last-writer-wins onstate.json— apausecould be silently dropped by a parallel session's hook (now a blocking lock, 0.3.1);resume(cursor now advances past them — paused means never traced);Specs live in
openspec/specs/; the five implemented changes are archived underopenspec/changes/archive/2026-07-02-*.