fix: unwrap workflow outputs in value_js + oauth2 in SSE server#564
fix: unwrap workflow outputs in value_js + oauth2 in SSE server#564
Conversation
…to SSE server Two bugs fixed: 1. Workflow output value_js received raw ReviewSummary wrappers instead of unwrapped step outputs. This caused every workflow tool (slack-search, slack-read-thread, discourse-read-thread, discourse-reply) to return "Unknown error" because outputs['step'].success was undefined (actual data was nested in .output). Script steps already unwrapped correctly via buildProviderTemplateContext, but workflow-executor's value_js, if conditions, and Liquid contexts did not. 2. executeHttpClientTool in mcp-custom-sse-server only handled bearer auth, not oauth2_client_credentials. When the AI called http_client tools with oauth2 auth (e.g. MongoDB Atlas), no token exchange happened and requests went out without Authorization headers, causing 401 errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request OverviewSummaryThis PR fixes two critical production bugs causing complete tool failure:
Files Changed AnalysisCore Bug Fixes (2 files)
Note: The PR description mentions Task Telemetry & Trace Improvements (4 files)
Observability Infrastructure (9 new files)
Other Improvements (7 files)
Architecture & Impact AssessmentOAuth2 Authentication FlowBefore: Only bearer token auth worked if (authType === 'bearer') {
headers['Authorization'] = `Bearer ${token}`;
}
// oauth2_client_credentials → 401 UnauthorizedAfter: OAuth2 client credentials flow added if (authType === 'bearer') {
headers['Authorization'] = `Bearer ${token}`;
} else if (authType === 'oauth2_client_credentials') {
const token = await tokenCache.getToken(tool.auth);
headers['Authorization'] = `Bearer ${token}`;
}OAuth2 flow: sequenceDiagram
participant AI as AI Provider
participant SSE as CustomToolsSSEServer
participant Cache as OAuth2TokenCache
participant API as OAuth2 Server
participant Target as Target API
AI->>SSE: call http_client tool (oauth2)
SSE->>Cache: getToken(config)
alt token cached & valid
Cache-->>SSE: cached token
else token expired/missing
Cache->>API: POST token_url
API-->>Cache: access_token
Cache-->>SSE: new token
end
SSE->>Target: GET api (Authorization: Bearer token)
Target-->>SSE: 200 OK
SSE-->>AI: tool result
Task Telemetry SystemNew task summary extraction:
Trace report structure: interface TraceReport {
traceData: ResolvedTraceData;
tree: string; // YAML-formatted span tree
taskSummary: ProbeTaskSummary | null;
headerText: string; // Task summary header
text: string; // Combined header + tree
}Observability StackMulti-container architecture: graph TB
subgraph "Visor App"
A[OTel SDK] -->|OTLP| B[4318 HTTP / 4317 gRPC]
end
subgraph "Observability Stack"
B --> C[OTel Collector]
C -->|Traces| D[Tempo :3200]
C -->|Metrics| E[Prometheus :9091]
D -->|Service Map| F[Grafana :8001]
E -->|Metrics| F
G[Autoheal] -.->|Health Checks| C
G -.->|Health Checks| D
G -.->|Health Checks| E
G -.->|Health Checks| F
end
Benefits over LGTM:
Scope Discovery & Context ExpansionDirect ImpactCritical fixes:
Indirect impact:
Production ImpactSeverity: Critical - complete tool failure Evidence from PR description:
Root cause: ReferencesCode LocationsSSE server changes:
Task telemetry changes:
Observability stack:
Related patterns (for context):
Documentation
Metadata
Powered by Visor from Probelabs Last updated: 2026-04-01T12:09:48.688Z | Triggered by: pr_updated | Commit: 0f69987 💡 TIP: You can chat with Visor using |
✅ Security Check PassedNo security issues found – changes LGTM. Performance Issues (3)
✅ Security Check PassedNo security issues found – changes LGTM. \n\n \n\nPerformance Issues (3)
Powered by Visor from Probelabs Last updated: 2026-04-01T11:59:16.050Z | Triggered by: pr_updated | Commit: 0f69987 💡 TIP: You can chat with Visor using |
- Fix on_message trigger dispatch to match normal message path: seed setFirstMessage so human_input checks auto-resolve and the full intent-router → build-config → generate-response chain runs with proper tool loading (Jira MCP, Slack, etc.) - Inject trigger.inputs.text as the AI message with original Slack message appended, so triggers can give specific instructions - Fix live update race condition: serialize publish() calls via a promise queue in SlackTaskLiveUpdateSink to prevent duplicate Slack messages when tick() and complete() run concurrently - Track inflightTick promise so complete()/fail() await in-flight ticks before publishing the final update - Fix self-bot message detection for bot_message subtypes by also checking ev.bot_id against the bot's own bot_id from auth.test - Add resolveChannelName() to SlackClient for #channel-name support in scheduler output targets via conversations.list with caching - Allow cron jobs without workflow (inputs.text as user message) - Make StaticCronJob.workflow optional in types - Fix workflow output warning to only fire for undefined (not null) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add debounce-manager for throttling check executions and integrate it into level-dispatch. Supports configurable throttle settings per check via config types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add global uncaughtException handler that suppresses transient I/O errors (EIO, EPIPE, ECONNRESET, ERR_STREAM_DESTROYED) from dying child processes instead of crashing the entire visor process. Three layers of defense: - Global handler in child-process-error-handler.ts (imported early) - Worktree manager skips process.exit(1) for transient I/O errors - Stream-level error handlers on MCP transport stderr pipes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update @probelabs/probe to v0.6.0-rc313 with enriched task telemetry (agent scope fields, full task state on events, task.items_json) - Parse task.items_json from batch events for proper titles on batch created/updated/completed/deleted operations - Collapse sub-agent scopes (engineer, code-explorer) that lack meaningful task titles into deduplicated single-line entries instead of showing repetitive generic "Engineer Task" items - Preserve sub-agent task titles when they exist (from task tool snapshots) - Group repeated sub-agent iterations under a single scope label Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Workflow output unwrapping bug:
value_jsin workflow outputs received rawReviewSummarywrappers ({ issues: [], output: {...} }) instead of unwrapped step results. This caused all workflow tools (slack-search, slack-read-thread, discourse-read-thread, discourse-reply) to return{ success: false, error: "Unknown error" }becauseoutputs['step'].successwasundefined— the actual data was nested inside.output. AddedunwrapOutputs()helper (same logic asbuildProviderTemplateContext) and applied it tovalue_js,ifconditions, Liquid templates, and expression mappings.OAuth2 in SSE server:
executeHttpClientToolonly handledbearerauth, missingoauth2_client_credentials. When the AI called http_client tools with oauth2 auth (e.g. MongoDB Atlasatlas-api), no token exchange happened, causing 401 Unauthorized.Impact
This was causing complete tool failure in production. A traced task (e6a694b7) showed the AI calling slack-search 4x and slack-read-thread 9x over 7 minutes — every call returned "Unknown error", and the AI produced empty output
{"text":""}.Test plan
visor test --no-mocks --only discourse-read-real— passes, returns real thread contentvisor test --no-mocks --only atlas-list-projects-real— passes, oauth2 token exchange worksvisor test --only discourse-skill-activation— passes with mocks🤖 Generated with Claude Code