Skip to content

fix(mcp): await the telemetry flush so events are sent before the CLI returns - #8727

Closed
tryeverything24 wants to merge 1 commit into
JSONbored:mainfrom
tryeverything24:fix-mcp-telemetry-flush-8690
Closed

fix(mcp): await the telemetry flush so events are sent before the CLI returns#8727
tryeverything24 wants to merge 1 commit into
JSONbored:mainfrom
tryeverything24:fix-mcp-telemetry-flush-8690

Conversation

@tryeverything24

Copy link
Copy Markdown
Contributor

Closes #8690

What

recordMcpToolCall in packages/loopover-mcp/lib/telemetry.ts constructed a PostHog client, called capture(...), and returned synchronously — fire-and-forget, with no awaited flush. capture() returns before the network POST lands, so if the --stdio process exits shortly after a tool call (client disconnect, process kill), the in-flight event is silently dropped. The remote counterpart (src/mcp/telemetry.ts) was already fixed this way in 80f4aec99/#7233; this PR gives the local wrapper the same guarantee.

How

  • packages/loopover-mcp/lib/telemetry.tsrecordMcpToolCall is now async and does await client.flush() after capture(...), mirroring the remote src/mcp/telemetry.ts fix exactly (same flushAt: 1, flushInterval: 0 config, same flush-before-resolve contract, same never-throw/never-reject catch). In the installed posthog-node v5, flush() is the public completion API (shutdown is underscore-private), so flush() is what the SDK offers a short-lived process — and it is what the remote wrapper uses.
  • packages/loopover-mcp/bin/loopover-mcp.ts — the guarantee is threaded through the single chokepoint: recordStdioToolTelemetry awaits recordMcpToolCall, and registerStdioTool awaits the telemetry on both its success and throw paths, so the event is on the wire before the stdio tool response goes out.

Client-reuse decision (stated per the issue): I kept per-call client construction rather than caching a single client. Reusing one instance would complicate the flush-on-exit guarantee (a shared client's queue couples calls together, and cache invalidation on env change adds a second code path), while the memory concern is already resolved by the awaited flush: with flushAt: 1, flushInterval: 0 and the flush awaited to completion, each client holds no queued events, timers, or pending work by the time recordMcpToolCall resolves, so it is immediately collectible — nothing accumulates over a long session. This also keeps the local wrapper byte-for-byte parallel to the remote one.

Deliverables

  • recordMcpToolCall awaits a flush before returning — its caller knows the event has been sent (or definitively failed) before the process can exit.
  • New test in test/unit/mcp-local-telemetry.test.ts ("does not resolve until the mocked flush/network call completes (fix(mcp): local MCP CLI telemetry never awaits/flushes its PostHog client before the process can exit #8690)"): the mocked flush resolves on a delay; the test proves the returned promise is still pending after the capture while the mocked network call is in flight, and resolves only after it completes. Also added: flush-called assertion on the happy path and a "never rejects when flush itself fails" case, mirroring test/unit/mcp-telemetry.test.ts.
  • No regression when telemetry is disabled/opted out: all four opt-out/unconfigured tests are unchanged in behavior (no construct, no capture, and now additionally assert no flush); the opt-out path still returns immediately.

Before / after

On unfixed main (fix stashed), the updated test file fails 5/13 — including the new pending-promise test, because recordMcpToolCall returned undefined synchronously:

FAIL  test/unit/mcp-local-telemetry.test.ts  5 failed | 8 passed (13)

With the fix:

✓ test/unit/mcp-local-telemetry.test.ts (13 tests)
✓ test/unit/mcp-telemetry.test.ts (9 tests)

The subprocess chokepoint suite (test/unit/mcp-local-telemetry-chokepoint.test.ts, real PostHog SDK against a local recorder) and the in-process mcp-cli suites (which exercise both recordStdioToolTelemetry call sites, success and throw) also pass unchanged.

…pToolCall resolves

recordMcpToolCall constructed a PostHog client, called capture(), and
returned synchronously -- fire-and-forget, with no awaited flush. Since
capture() returns before the network POST lands, a --stdio process that
exits shortly after a tool call silently dropped the in-flight event.

Mirror the remote src/mcp/telemetry.ts fix (80f4aec, JSONbored#7233) exactly:
recordMcpToolCall is now async and awaits client.flush() after capture,
inside the same never-throw catch. The guarantee is threaded through the
stdio chokepoint -- recordStdioToolTelemetry awaits the wrapper and
registerStdioTool awaits it on both the success and throw paths -- so
the event is on the wire before the tool response goes out.

Per-call client construction is kept (not cached): with flushAt 1,
flushInterval 0 and the flush awaited to completion, each client holds
no queued events or pending work by the time the promise resolves, so
nothing accumulates across a long session.

Closes JSONbored#8690
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

1 similar comment
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 25.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.30%. Comparing base (abf88ab) to head (c351882).

Files with missing lines Patch % Lines
packages/loopover-mcp/bin/loopover-mcp.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8727      +/-   ##
==========================================
- Coverage   90.56%   82.30%   -8.26%     
==========================================
  Files          96       98       +2     
  Lines       22490    24762    +2272     
  Branches     3884     4746     +862     
==========================================
+ Hits        20367    20381      +14     
- Misses       1945     4203    +2258     
  Partials      178      178              
Flag Coverage Δ
backend 0.61% <25.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-mcp/lib/telemetry.ts 100.00% <100.00%> (ø)
packages/loopover-mcp/bin/loopover-mcp.ts 0.00% <0.00%> (ø)

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 26, 2026
@loopover-orb

loopover-orb Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-26 01:15:27 UTC

3 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Fix Blockers

Review summary
This PR makes recordMcpToolCall async and awaits client.flush() after capture(), then threads that await through recordStdioToolTelemetry and both success/throw paths of registerStdioTool, exactly mirroring the already-shipped remote fix in src/mcp/telemetry.ts/#7233. The change is correct and narrowly scoped: the never-throw contract is preserved (flush is inside the same try/catch), and the test suite adds a real deferred-flush test that observes the returned promise staying pending until the mocked network call resolves, which is genuine coverage of the actual behavior change rather than a fabricated scenario. The one real gap is that awaiting flush on the hot path now adds PostHog's network round-trip latency directly to every stdio tool response when telemetry is enabled, which isn't discussed.

Nits — 5 non-blocking
  • packages/loopover-mcp/bin/loopover-mcp.ts:1734-1739 — awaiting recordStdioToolTelemetry synchronously blocks the tool's return until the PostHog flush completes (or times out), adding real network latency to every stdio response when telemetry is enabled; worth a brief note on the accepted latency tradeoff or a bounded timeout, since the caller can't opt out of the wait even though telemetry itself is opt-in.
  • codecov/patch flags only 25% of the diff hit — the new `does not resolve until flush completes` test covers the promise-timing branch, but confirm the `throwOnFlush`/`throwOnConstruct` catch branches in bin/loopover-mcp.ts's own await paths (success vs throw) are each independently exercised, not just telemetry.ts's.
  • packages/loopover-mcp/bin/loopover-mcp.ts:1720 introduces `any` typing on `recordStdioToolTelemetry`'s params, consistent with the rest of this pre-existing untyped file but worth tightening opportunistically if touched again.
  • Consider whether a short flush timeout (e.g. Promise.race against a small deadline) is warranted so a slow/hanging PostHog endpoint can't stall every tool response indefinitely, matching the fail-open posture of the rest of this module.
  • If patch coverage must hit ~97%, add a targeted test asserting registerStdioTool's throw path (bin/loopover-mcp.ts) actually awaits recordStdioToolTelemetry before rethrowing, not just that telemetry.ts's flush is awaited in isolation.

CI checks failing

  • codecov/patch — 25.00% of diff hit (target 99.00%)

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #8690
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 88 registered-repo PR(s), 40 merged, 4 issue(s).
Contributor context ✅ Confirmed Gittensor contributor tryeverything24; Gittensor profile; 88 PR(s), 4 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
recordMcpToolCall is now async and awaits client.flush() before returning, mirroring the remote fix, and the chokepoint in loopover-mcp.ts propagates the await through recordStdioToolTelemetry/registerStdioTool; a new test with a delayed flush explicitly asserts the returned promise stays pending until the mocked network call completes, and the disabled/opt-out no-op paths retain their existing as

Review context
  • Author: tryeverything24
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: JavaScript, TypeScript, Python, HTML, C++, Java, PHP, C#
  • Official Gittensor activity: 88 PR(s), 4 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb

loopover-orb Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

LoopOver is closing this pull request on the maintainer's behalf (CI is failing (codecov/patch)). This is an automated maintenance action — to pursue this change, please open a new pull request with the issues resolved. Closed PRs may be analyzed later to improve review accuracy, but they are not automatically reopened or re-reviewed.

@loopover-orb loopover-orb Bot closed this Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(mcp): local MCP CLI telemetry never awaits/flushes its PostHog client before the process can exit

1 participant