Skip to content

feat(telemetry): each captured payload stored in the background is one operation (#733) - #985

Merged
github-actions[bot] merged 3 commits into
mainfrom
feat/733-capture-operation-id
Nov 11, 2026
Merged

github-actions[bot] merged 3 commits into
mainfrom
feat/733-capture-operation-id

Conversation

@cdeust

@cdeust cdeust commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Refs #733 (slice 4; the issue stays open, see "Still open").

What

The part of "operation identity across background processing" that needs no change to the capture frame.

  • New telemetry.begin_operation() in mcp_server/core/telemetry.py: starts a fresh operation in the current context, lasting until the next call or the end of the context.
  • The resident worker's callback (capture_store.remember) and the spool drainer (capture_drain.drain_pending) call it once per payload, before the store.
  • Result: the remember sample of a capture, its capture_processed, and the capture_skipped of a failed or refused payload share one operation_id; it was null. Two payloads get distinct ids.
  • The id is born in the worker. It does not name the hook invocation that produced the payload; the CHANGELOG entry and the audit say so.

Proof

tests_py/hooks/test_capture_operation_identity_733.py (6 tests) and 3 tests added to tests_py/core/test_operation_identity_733.py. With either begin_operation() call removed, 3 of the 6 fail (the worker tests for one, the drainer tests for the other).

The worker tests use no feeder thread and no idle-timeout margin: the first payload is queued before the consume loop starts, and the test callback queues the next one from the server's own task.

Checks

  • The two test files: 13 passed. The hook file alone, 71 runs across loops: no failure, no error.
  • ruff check, ruff format --check on the changed files: clean. scripts/check_craftsmanship.py: OK. scripts/check_doc_claims.py: OK.
  • Mutation, with the CI's own plan on the diff: begin_operation and capture_store.remember have no survivor; capture_drain.drain_pending has none on the changed lines (7 survivors on lines this change does not touch are not judged, ADR-1122). No equivalent registered.
  • After merging main: the 733 tests, the backup tests and the handler selection pass (70 passed).

Not verified

  • Windows, where the drainer actually runs.
  • pyright and the full suite were not run locally; this pull request's CI runs the suite.
  • The mutation figures come from a local run with mutmut 3.8.1, not from the CI's own job.

Still open on #733

  1. An id for a hook invocation and its hand-off to the worker. The capture payload has a fixed key set (capture_worker_policy.validate_payload) and no version, and a resident worker started before an upgrade keeps the old code: it would acknowledge a new payload and then refuse it. This needs a decision: version the frame, restart the worker on code change, or accept the loss.
  2. Background cycles (consolidate) carry no id.
  3. Retry count per operation: the issue does not say which retries, and no code path defines an attempt.
  4. CPU time beside wall time: the issue names no measurement method.
  5. Provider usage beyond total_cost_usd.

Generated by Claude Code

…e operation (#733, slice 4)

The resident capture worker and the spool drainer run outside any MCP call, so
every sample they recorded had operation_id null: the remember sample of a
capture, its capture_processed and the capture_skipped of its failure were
joined only by op and ts.

telemetry.begin_operation() starts a fresh operation in the current context,
which lasts until the next call or the end of the context. The worker's
callback (capture_store.remember) and the drainer (capture_drain.drain_pending)
call it once per payload, before the store. The capture_skipped the server
reports from the same task when the store raises, and the one the drainer
reports when it retires a file, therefore carry the id of the payload they
are about.

The id is born where the payload is stored. It does not name the hook
invocation that produced the payload: the capture payload has a fixed key set
and no version, and a resident worker started before an upgrade keeps running
the old code, so it would refuse a payload carrying a new key. That hand-off
needs a decision on the frame and is left out; the audit says so.

Tests drive the resident server's own consume loop and the real drainer, one
of them through the real remember handler on SQLite. Mutation gate run locally
on the changed functions with the CI plan and test selection: no survivor on a
changed line.

Not verified here: Windows (the drainer's platform), pyright, the full suite
(torch cannot be installed in this container).

Refs #733

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYrKg46KKYpvLPXcW4M
Signed-off-by: Claude <[email protected]>
…le timeout (#733)

The three worker tests fed the server's bounded admission queue from a thread
and relied on the consume loop meeting the next payload within a 0.5 s idle
timeout: on a slow runner the loop could stop first and the test fail.

The first payload is now queued before the loop starts, and the callback
queues the next one once a payload has been processed (the queue is empty
then) or stops the server after the last. put_nowait fails, it cannot block,
and the idle timeout is never waited for. The callback still awaits
capture_store.remember in the server's own task, so the tests fail as before
when either begin_operation() call is removed.

Refs #733

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYrKg46KKYpvLPXcW4M
Signed-off-by: Claude <[email protected]>
CHANGELOG.md conflict: both entries are kept, main's first.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYrKg46KKYpvLPXcW4M
Signed-off-by: Claude <[email protected]>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T10:31:12.556674Z 0225e31 PR opened
🔒 Security Review ✅ Completed 2026-11-11T10:32:49.775170Z 0225e31 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 3m 31s —— View job


PR #985 review (1 of 3)

I found no blockers, and the review lists three one-line notes. I could not run pytest or ruff in this job because shell commands were not approved. The Pipeline CI was still running when the review was posted.

Verdict: approve

@claude

claude Bot commented Nov 11, 2026

Copy link
Copy Markdown

Review 1 of 3: PR #985 (operation identity for background captures, #733 slice 4)

I found no blockers. I ran three reviews (code quality, test coverage, documentation accuracy) and traced the context-variable paths myself:

  • Worker: CaptureServer._consume awaits the callback in its own task (capture_server.py:139). The id that begin_operation() sets in remember therefore reaches the capture_skipped report in the except block.
  • Drainer: _refuse runs in the same task as the begin_operation() call (capture_drain.py:66), on Python 3.11 and 3.12+.
  • Leftover id: after a payload, its id stays in the consume loop's context. Only _next (through to_thread) inherits it, and _next records nothing. The receiver thread starts with an empty context.
  • Tests: they fail if either begin_operation() call is removed, and the worker tests cannot hang.
  • Layers and gate: the layer rules and the craftsmanship limits are respected.

I could not run pytest or ruff in this job (no approval for shell commands). The PR's Pipeline CI was still running when I posted this.

Notes, no action before merge

  • CHANGELOG.md:78-80: a payload refused on receipt (authentication, size, full queue; capture_server.py:59-64, receiver thread) still logs operation_id: null, and the drainer also creates ids, not only the worker.
  • telemetry.py:91: the comment "None outside (hooks)" leaves out the worker's admission and lifecycle samples and MCP requests other than tools/call.
  • test_capture_operation_identity_733.py:107: the {} test does not check the ops of the later samples or why the payload was refused; test_operation_identity_733.py:107 int(..., 16) >= 0 is always true.

View job run
Verdict: approve

@github-actions
github-actions Bot merged commit 52b588a into main Nov 11, 2026
18 checks passed
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.

2 participants