feat: W3C traceparent propagation for A2A delegations (#7)#31
Merged
feat: W3C traceparent propagation for A2A delegations (#7)#31
Conversation
## What's in this PR ### New: src/telegram-notify.ts - Extracts relayTaskUpdateToTelegram() from a2a-server.ts to avoid circular imports - Adds relayJobCompletionToTelegram() with rich completion/failure/timeout messages - Adds formatDuration(ms) helper: <60s → '42s', <1h → '1m 42s', ≥1h → '2h 5m' - Adds summarizeResult(text, maxChars?) helper: truncates with '…' suffix ### Updated: src/tools/claude-subagent.ts (issue #26) - Adds receiveCallback(jobId, status, result?, error?) → boolean - Updates in-memory job map + persists to disk - Fires Telegram notification unless job.silent=true - Returns false for unknown jobIds (used by HTTP endpoint for 404) - Wires receiveCallback into close/error/timeout handlers (parent-process approach) - Adds silent?: boolean field to DelegateJob - Exports _buildAnthropicCustomHeadersForTest, _clearJobsForTest, _addJobForTest for tests - Adds silent param to delegateToClaudeSubagent tool ### Updated: src/a2a-server.ts (issue #26 + #5) - Adds POST /tasks/callback endpoint (auth-gated) for cross-machine callbacks (Nix → Max) - Worker complete/error handlers now use relayJobCompletionToTelegram with duration tracking - Adds isSilent + workerStartTime tracking for A2A worker tasks - Agent card capabilities now includes callback_endpoint: true ### New tests - tests/telegram-notify.test.ts — formatDuration edge cases, summarizeResult truncation - tests/a2a-callback.test.ts — 401/400/404/200 HTTP coverage for /tasks/callback - tests/worker.test.ts — extended with DelegateJob silent flag tests Closes #5 Closes #26
## What's implemented ### Core feature: Trace context propagation (issue #7) - A2A server now extracts W3C traceparent from incoming requests via OpenTelemetry propagation API - Incoming trace context is propagated through both sync and async task execution - Result: Max→Nix→Max delegations appear as linked parent-child spans in Grafana (unified trace) ### src/a2a-server.ts changes - Added OpenTelemetry imports: `context`, `propagation`, `trace` from '@opentelemetry/api' - POST /tasks endpoint: - Extracts traceparent from request headers: `propagation.extract(context.active(), req.headers)` - Wraps async worker execution in extracted context: `context.with(incomingContext, executeWorker)` - Wraps sync task execution in extracted context for consistent propagation - Both paths (async worker, sync agent.prompt) now execute within the inherited trace - POST /tasks/stream endpoint now also extracts and propagates trace context ### Tests added - tests/a2a-trace-propagation.test.ts — comprehensive trace propagation test suite: - Valid W3C traceparent format extraction - Graceful fallback when traceparent is absent or malformed - Propagation through both sync and async task execution - AgentWeave headers + traceparent co-propagation (full context fidelity) - Trace context inheritance across worker execution ### Architecture note This is the Max-side of bidirectional trace propagation. When Nix delegates to Max (POST /tasks with traceparent header), the task runs within that trace context. Conversely, when Max delegates to Nix (via nix-relay.ts delegateToNix tool), it injects traceparent into the request headers. Result: full end-to-end trace visibility across machine boundaries (Max ← → Nix). **7/7 test suites, 65/65 tests passing.** Closes #7
…orkerData, honest test descriptions
Three fixes:
1. Remove unused 'trace' import from @opentelemetry/api in a2a-server.ts
Only 'context' and 'propagation' are used.
2. Serialize traceparent into workerData for cross-thread propagation
Worker threads run in a separate thread — AsyncLocalStorage context does not
cross thread boundaries. Previously context.with(incomingContext, ...) was a
no-op for the worker. Fix:
- a2a-server.ts: inject incomingContext into traceHeaders{} via propagation.inject(),
pass traceHeaders in workerData alongside existing fields
- worker.ts: re-extract trace context from traceHeaders via propagation.extract(),
run entire worker execution inside context.with(incomingContext, ...) so any
spans created by the worker are linked as children of the calling agent's trace
3. Rewrite test descriptions to be accurate
Previous tests just asserted HTTP status codes but were labelled as if they
verified actual span propagation. New tests:
- Unit: propagation round-trip (extract → re-inject → re-extract) — tests the
actual mechanism both a2a-server.ts and worker.ts rely on
- HTTP smoke: /tasks endpoint accepts traceparent without erroring
- Add NODE_ENV=test guard to relayTaskUpdateToTelegram and relayJobCompletionToTelegram Prevents real Telegram messages firing when TELEGRAM_BOT_TOKEN is set in .env and tests inherit the env (which they do — caused the task failure spam to Telegram) - Explicitly set NODE_ENV=test in jest.config.cjs testEnvironmentOptions for safety
jest.mock() silently fails for ESM modules in Jest 29 + ts-jest. Mocks were never applied, masked locally by ~/max/data/ existing. In CI the real task-journal.ts tried to open a nonexistent DB path and threw. Switch all jest.mock() calls to jest.unstable_mockModule() in both a2a-trace-propagation and a2a-callback test files. Simplify callback tests to use mockReturnValueOnce instead of _addJobForTest helpers (which were no-op mocks). Co-Authored-By: Claude Opus 4.6 (1M context) <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.
Summary
Implements bidirectional W3C traceparent propagation for Max ↔ Nix A2A delegations. Traces now appear as linked parent-child spans in Grafana, providing end-to-end visibility across machine boundaries.
Changes
src/a2a-server.tscontext,propagationfrom '@opentelemetry/api'propagation.extract()context.with(incomingContext, executeWorker)Tests
00-traceId-parentId-flags)How it works
delegateToNixalready injects traceparent viapropagation.inject()7/7 test suites, 65/65 tests passing.
Closes #7