Skip to content

Privacy controls, integration test harness, and state-locking fixes#4

Merged
winjer merged 9 commits into
mainfrom
privacy-changes
Jul 10, 2026
Merged

Privacy controls, integration test harness, and state-locking fixes#4
winjer merged 9 commits into
mainfrom
privacy-changes

Conversation

@winjer

@winjer winjer commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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:

  • Track 2 (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.
  • Track 1 (harness/): the real pinned claude CLI 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; the harness CI job in this PR is its first in-Docker run.

Fixes, each landed red-test-first:

  • the state flock passed LOCK_NB and ignored the result, so concurrent invocations were last-writer-wins on state.json — a pause could be silently dropped by a parallel session's hook (now a blocking lock, 0.3.1);
  • turns completed while a session was paused were replayed to Langfuse on resume (cursor now advances past them — paused means never traced);
  • the first hook for a never-seen session emitted the entire pre-existing transcript, so a fresh install uploaded pre-install history (first contact now emits only the turn that fired the hook).

Specs live in openspec/specs/; the five implemented changes are archived under openspec/changes/archive/2026-07-02-*.

winjer and others added 9 commits July 1, 2026 21:31
 - 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>
@winjer winjer marked this pull request as ready for review July 2, 2026 15:23
@winjer winjer merged commit 0485969 into main Jul 10, 2026
2 checks passed
@winjer winjer deleted the privacy-changes branch July 10, 2026 08:12
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