Skip to content

fix: [AI-7743] Altimate Code chat — MCP servers not getting added (question hang)#1007

Open
saravmajestic wants to merge 1 commit into
mainfrom
fix/AI-7743-mcp-add-question-hang
Open

fix: [AI-7743] Altimate Code chat — MCP servers not getting added (question hang)#1007
saravmajestic wants to merge 1 commit into
mainfrom
fix/AI-7743-mcp-add-question-hang

Conversation

@saravmajestic

@saravmajestic saravmajestic commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes AI-7743 — in Altimate Code chat, /discover-and-add-mcps showed the "which MCP / what scope" question, but after the user answered it the chat sat on "Thinking…" forever and the MCP server was never added.

Root cause

After the v1.17.9 upstream merge (effect@4), src/question/index.ts is bundled as two separate module instances. Proven: a module-scoped id logged from the question tool's ask() differs from the one logged in the HTTP reply() route; normalizing every importer to @/question did not make Bun dedupe it. Each copy ran its own makeRuntime runtime, which broke the flow in two independent ways:

  1. State split. The question tool registered the pending Deferred in one copy's InstanceState pending-map, while POST /question/:id/reply looked it up in the other copy's empty map. The reply returned NotFoundError (HTTP 500) and the Deferred never resolved, so the agent loop blocked forever → "Thinking…". (GET /question returned [] for the same reason.)
  2. Event never reached the IDE webview. question.asked was published only via EventV2BridgeGlobalBus, but the /event SSE stream the webview subscribes to is fed by the Bus wildcard PubSub (Bus.publish) — a different channel. So the webview's pendingQuestions stayed empty, the answer card's Submit resolved requestId: undefined, and submitting did nothing.

Either half alone produces the reported "answer the question → nothing happens".

Fix (all in question/index.ts + import cleanup)

  • State: anchor the pending-question registry on globalThis (keyed by instance directory) so all module copies share one map. Restore per-instance cleanup via registerDisposer so entries don't leak across instances/tests.
  • Events: publish question.asked / replied / rejected via Bus.publish (added BusEvent mirrors) so they reach /event like every other webview-visible event.
  • Normalize the remaining relative ../question imports to @/question.

Verification

End-to-end in Docker code-server (VS Code extension + real UI):

  • /discover-and-add-mcps → question card renders → answer Yes / Project✅ datamate added to .altimate-code/altimate-code.json (enabled: true) and the chat shows the success summary.
  • Confirmed the webview now receives the event (pendingQuestions populated, was []) and the reply resolves (HTTP 200, message=replied).
  • 55 question unit tests pass (test/question, test/tool/question, test/release-validation/question-937*, test/cli/run/question.shared).

No changes were needed in the vscode-altimate-mcp-server extension.

🤖 Generated with Claude Code


Summary by cubic

Fixes AI-7743: answering the MCP add question in /discover-and-add-mcps no longer hangs. We now share pending-question state across bundled module copies and mirror question events on the Bus so the webview receives them and the MCP server is added.

  • Bug Fixes
    • Share the pending-question registry on globalThis (keyed by instance directory) and dispose per instance via registerDisposer so ask() and HTTP reply() resolve the same request.
    • Publish question.asked / replied / rejected via Bus.publish (new BusEvent mirrors) so /event delivers them to the IDE webview.

Written for commit cd96fea. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Question events are now available to IDE webview event stream subscribers, including asked, replied, and rejected states.
    • Question handling is shared reliably across modules within the same application instance.
  • Bug Fixes

    • Pending questions are now cleaned up when an instance is disposed.
    • Outstanding questions are automatically rejected instead of remaining unresolved.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Question pending state now uses a process-global, directory-keyed registry with disposal cleanup. Question ask, reply, and reject operations also publish corresponding wildcard Bus events for SSE subscribers.

Changes

Question flow

Layer / File(s) Summary
Shared question state and disposal
packages/opencode/src/question/index.ts
Pending questions move to a global directory-keyed registry; reply, reject, and list use it, while disposal rejects remaining requests and removes the directory entry.
Wildcard question events
packages/opencode/src/question/index.ts
Bus event schemas and publications are added for question.asked, question.replied, and question.rejected.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Question
  participant Bus
  participant EventSSE
  Question->>Bus: publish question.asked
  Bus->>EventSSE: deliver wildcard event
  Question->>Bus: publish question.replied or question.rejected
  Bus->>EventSSE: deliver wildcard event
Loading

Poem

I’m a rabbit with questions, hopping in line,
A global burrow keeps each thread fine.
Ask, reply, reject—events now fly,
Through Bus to the webview sky.
When paths close, pending hopes say goodbye.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main bug fix.
Description check ✅ Passed The description covers the issue, root cause, fix, and verification, matching the template’s required substance.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/AI-7743-mcp-add-question-hang

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/question/index.ts (1)

228-240: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make lifecycle finalization independent of event publication.

A failed publication can leak an asked request or strand a replied/rejected deferred.

  • packages/opencode/src/question/index.ts#L228-L240: wrap publication and awaiting with cleanup that begins immediately after pending.set.
  • packages/opencode/src/question/index.ts#L254-L269: guarantee Deferred.succeed with Effect.ensuring.
  • packages/opencode/src/question/index.ts#L279-L289: guarantee Deferred.fail with Effect.ensuring.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/question/index.ts` around lines 228 - 240, Make
question lifecycle cleanup independent of publication and deferred completion:
in packages/opencode/src/question/index.ts lines 228-240, wrap publication and
Deferred.await immediately after pending.set with Effect.ensuring that always
deletes the pending request; in lines 254-269, wrap Deferred.succeed with
Effect.ensuring; and in lines 279-289, wrap Deferred.fail with Effect.ensuring
so cleanup runs even when publication or completion fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/opencode/src/question/index.ts`:
- Around line 228-240: Make question lifecycle cleanup independent of
publication and deferred completion: in packages/opencode/src/question/index.ts
lines 228-240, wrap publication and Deferred.await immediately after pending.set
with Effect.ensuring that always deletes the pending request; in lines 254-269,
wrap Deferred.succeed with Effect.ensuring; and in lines 279-289, wrap
Deferred.fail with Effect.ensuring so cleanup runs even when publication or
completion fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 22566e2a-fd12-4a52-86af-c5cc09552b64

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc8850 and ab2bd9f.

📒 Files selected for processing (4)
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/routes/question.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/tool/question.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

@kilo-code-bot

kilo-code-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Re-verified after the branch was force-pushed (previous review SHA no longer reachable). The change is now a single self-contained commit on question/index.ts; the previously-reviewed import normalizations were folded in. The AI-7743 fix is correct and verified against the shipped wiring:

  • State sharing: pendingByDir is anchored on globalThis (keyed by instance directory) with ??=, so all bundled copies of question/index.ts share one map; pendingFor(directory) keeps list() per-instance.
  • Disposal hygiene: registerDisposer + Effect.addFinalizer(() => Effect.sync(off)) mirrors the exact pattern already used by InstanceState.make (instance-state.ts:38-39). The disposer runs on Instance.dispose/reload (project/instance.ts:144,135), is idempotent across module copies (shared map; the second copy sees the directory already deleted), and each Deferred.fail is run via Effect.runPromise(...).catch(() => {}).
  • Event delivery: the shipped /event SSE route reads only Bus.subscribeAll (server/routes/event.ts:64), which Bus.publish populates and events.publish/EventV2Bridge does not — so question.asked/replied/rejected reach the IDE webview exactly once. events.publish is still needed (it persists to EventTable and emits sync events that Bus.publish does not), so it is not redundant.
  • No double-delivery: Bus.publish also emits to GlobalBus, but every active question consumer dedupes by id — TUI notifications.ts:36 and sync.tsx:290 both key on the question/request id.
  • Effect correctness: Effect.promise(() => Bus.publish(...)) is yield*-run, never bare-awaited.
Files Reviewed (1 file)
  • packages/opencode/src/question/index.ts
Previous Review Summary (commit ab2bd9f)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit ab2bd9f)

Status: No Issues Found | Recommendation: Merge

The fix correctly addresses the AI-7743 hang from two angles, both verified against the actual shipped wiring:

  • State sharing: anchoring the pending-question registry on globalThis (keyed by instance directory) makes all bundled module copies of question/index.ts share one map. ??= makes init idempotent across copies, and disposeInstance(directory) is called on instance dispose/reload (project/instance.ts:135,144), so the registerDisposer cleanup runs and the disposer is idempotent if both copies register.
  • Event delivery: the shipped /event SSE route is fed by Bus.subscribeAll (wildcard PubSub), which Bus.publish populates — so BusAsked/Replied/Rejected now reach the IDE webview. events.publish/EventV2Bridge does not feed that PubSub, so there's no double-delivery.
  • Effect/Promise correctness: Effect.promise(() => Bus.publish(...)) is properly yield*-run (not bare-awaited), and Deferred.fail in the disposer is executed via Effect.runPromise.

Import normalizations (../question@/question) are mechanical and consistent.

Files Reviewed (4 files)
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/routes/question.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/tool/question.ts

Reviewed by glm-5.2 · Input: 85K · Output: 22.3K · Cached: 1.4M

Review guidance: REVIEW.md from base branch main

…bus across bundled module copies

`/discover-and-add-mcps` rendered the "which MCP / what scope" question but,
after answering, the chat sat on "Thinking…" and the server was never added.

Root cause: after the v1.17.9 upstream merge, `src/question/index.ts` is bundled
as TWO separate module instances (a module-scoped id differs between the tool's
`ask()` and the HTTP route's `reply()`; Bun does not reliably dedupe it, and
normalizing imports does NOT change that). Each copy ran its own `makeRuntime`
runtime, which broke the flow two independent ways:

1. State split — the `question` tool registered the pending `Deferred` in one
   copy's `InstanceState` map, while `POST /question/:id/reply` looked it up in
   the OTHER copy's empty map. The reply 404'd and the `Deferred` never resolved,
   so the agent loop blocked forever ("Thinking…").
2. Event never reached the IDE webview — `question.asked` was published only via
   `EventV2Bridge` -> `GlobalBus`, but the `/event` SSE stream the webview reads
   is fed by the Bus *wildcard PubSub* (`Bus.publish`). So `pendingQuestions`
   stayed empty and the answer card's Submit had no request id -> submitting did
   nothing.

Fix (self-contained in `question/index.ts`):
- Anchor the pending-question registry on `globalThis` (keyed by instance
  directory) so every bundled module copy shares one map. Restore per-instance
  cleanup via `registerDisposer` so entries don't leak across instances/tests.
- Publish `question.asked`/`replied`/`rejected` via `Bus.publish` (added
  `BusEvent` mirrors) so they reach `/event` like every other webview-visible
  event.

Every divergence from upstream is wrapped in `altimate_change start/end`
markers (14/14 balanced) so future upstream merges are handled correctly;
`--require-markers --strict` and the marker-integrity tests pass.

Verified end-to-end in code-server: discover -> answer (Yes / Project) ->
datamate written to `.altimate-code/altimate-code.json` (enabled) and the chat
shows the success summary. Marker-integrity + question tests pass (134).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saravmajestic saravmajestic force-pushed the fix/AI-7743-mcp-add-question-hang branch from ab2bd9f to cd96fea Compare July 16, 2026 05:28
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@saravmajestic saravmajestic self-assigned this Jul 16, 2026

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Code Review — OpenCodeReview (Gemini) — 4 finding(s)

  • 4 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/question/index.ts (L266-L274)

[🔴 HIGH] If Bus.publish throws or rejects (e.g., due to bus errors), this Effect will terminate early, and Deferred.succeed will never be reached. This will cause the original ask operation that awaits this deferred to hang indefinitely.

Consider decoupling side-effects like event mirroring by using a non-blocking fork (e.g., Effect.fork(Effect.promise(...))), or placing it after Deferred.succeed, or applying explicit error handling (e.g., Effect.ignoreLogged / Effect.catchAllDefect) so it doesn't block the core business logic.

Suggested change:

      // altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
      yield* Effect.fork(Effect.promise(() =>
        Bus.publish(BusReplied, {
          sessionID: existing.info.sessionID,
          requestID: existing.info.id,
          answers: input.answers.map((a) => [...a]),
        }),
      ))
      // altimate_change end

2. packages/opencode/src/question/index.ts (L293-L297)

[🔴 HIGH] Similarly, if Bus.publish throws or rejects here, the Effect will terminate early, preventing Deferred.fail from being executed and leaving the pending ask operation hanging indefinitely.

Consider placing this in a non-blocking fork or adding explicit error handling to avoid blocking the rejection resolution.

Suggested change:

      // altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
      yield* Effect.fork(Effect.promise(() =>
        Bus.publish(BusRejected, { sessionID: existing.info.sessionID, requestID: existing.info.id }),
      ))
      // altimate_change end

3. packages/opencode/src/question/index.ts (L110-L118)

[🟠 MEDIUM] The use of any type (z.any()) is strictly prohibited by our review checklist unless explicitly justified. Using any bypasses Zod's validation and type inference, potentially allowing unexpected data structures to propagate.

Please use z.unknown() or define a stricter Zod schema that matches the expected structure of Question.Info to maintain type safety.

Suggested change:

const BusAsked = BusEvent.define(
  "question.asked",
  z.object({
    id: QuestionID.zod,
    sessionID: SessionID.zod,
    questions: z.array(z.unknown()),
    tool: z.object({ messageID: MessageID.zod, callID: z.string() }).optional(),
  }),
)

4. packages/opencode/src/question/index.ts (L204-L206)

[🟠 MEDIUM] This promise resolution contains an empty catch block: .catch(() => {}). This silently swallows any unexpected errors that might occur when Effect.runPromise executes, which violates error-handling best practices and can obscure underlying bugs during instance cleanup.

Consider logging the error explicitly instead of silently ignoring it, e.g., .catch((e) => log.error(e)).

Suggested change:

      for (const { deferred } of map.values()) {
        await Effect.runPromise(Deferred.fail(deferred, new RejectedError())).catch(console.error)
      }

Comment on lines +266 to +274
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.promise(() =>
Bus.publish(BusReplied, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
answers: input.answers.map((a) => [...a]),
}),
)
// altimate_change end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔴 HIGH] If Bus.publish throws or rejects (e.g., due to bus errors), this Effect will terminate early, and Deferred.succeed will never be reached. This will cause the original ask operation that awaits this deferred to hang indefinitely.

Consider decoupling side-effects like event mirroring by using a non-blocking fork (e.g., Effect.fork(Effect.promise(...))), or placing it after Deferred.succeed, or applying explicit error handling (e.g., Effect.ignoreLogged / Effect.catchAllDefect) so it doesn't block the core business logic.

Suggested change:

Suggested change
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.promise(() =>
Bus.publish(BusReplied, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
answers: input.answers.map((a) => [...a]),
}),
)
// altimate_change end
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.fork(Effect.promise(() =>
Bus.publish(BusReplied, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
answers: input.answers.map((a) => [...a]),
}),
))
// altimate_change end

Comment on lines +293 to +297
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.promise(() =>
Bus.publish(BusRejected, { sessionID: existing.info.sessionID, requestID: existing.info.id }),
)
// altimate_change end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔴 HIGH] Similarly, if Bus.publish throws or rejects here, the Effect will terminate early, preventing Deferred.fail from being executed and leaving the pending ask operation hanging indefinitely.

Consider placing this in a non-blocking fork or adding explicit error handling to avoid blocking the rejection resolution.

Suggested change:

Suggested change
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.promise(() =>
Bus.publish(BusRejected, { sessionID: existing.info.sessionID, requestID: existing.info.id }),
)
// altimate_change end
// altimate_change start — mirror on the Bus wildcard for /event (webview) clients.
yield* Effect.fork(Effect.promise(() =>
Bus.publish(BusRejected, { sessionID: existing.info.sessionID, requestID: existing.info.id }),
))
// altimate_change end

Comment on lines +110 to +118
const BusAsked = BusEvent.define(
"question.asked",
z.object({
id: QuestionID.zod,
sessionID: SessionID.zod,
questions: z.array(z.any()),
tool: z.object({ messageID: MessageID.zod, callID: z.string() }).optional(),
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 MEDIUM] The use of any type (z.any()) is strictly prohibited by our review checklist unless explicitly justified. Using any bypasses Zod's validation and type inference, potentially allowing unexpected data structures to propagate.

Please use z.unknown() or define a stricter Zod schema that matches the expected structure of Question.Info to maintain type safety.

Suggested change:

Suggested change
const BusAsked = BusEvent.define(
"question.asked",
z.object({
id: QuestionID.zod,
sessionID: SessionID.zod,
questions: z.array(z.any()),
tool: z.object({ messageID: MessageID.zod, callID: z.string() }).optional(),
}),
)
const BusAsked = BusEvent.define(
"question.asked",
z.object({
id: QuestionID.zod,
sessionID: SessionID.zod,
questions: z.array(z.unknown()),
tool: z.object({ messageID: MessageID.zod, callID: z.string() }).optional(),
}),
)

Comment on lines +204 to +206
for (const { deferred } of map.values()) {
await Effect.runPromise(Deferred.fail(deferred, new RejectedError())).catch(() => {})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 MEDIUM] This promise resolution contains an empty catch block: .catch(() => {}). This silently swallows any unexpected errors that might occur when Effect.runPromise executes, which violates error-handling best practices and can obscure underlying bugs during instance cleanup.

Consider logging the error explicitly instead of silently ignoring it, e.g., .catch((e) => log.error(e)).

Suggested change:

Suggested change
for (const { deferred } of map.values()) {
await Effect.runPromise(Deferred.fail(deferred, new RejectedError())).catch(() => {})
}
for (const { deferred } of map.values()) {
await Effect.runPromise(Deferred.fail(deferred, new RejectedError())).catch(console.error)
}

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

🤖 Code Review — OpenCodeReview (Gemini) — No Issues Found

No supported files changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants