Skip to content

fix: a failed prompt leaves no workspace verdict, and the workspace telemetry review's follow-ups - #1711

Open
ralphstodomingo wants to merge 4 commits into
mainfrom
fix/workspace-telemetry-review-minors
Open

ralphstodomingo wants to merge 4 commits into
mainfrom
fix/workspace-telemetry-review-minors

Conversation

@ralphstodomingo

@ralphstodomingo ralphstodomingo commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #1698

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

  • A failed prompt leaves no workspace verdict. A failed attachment cleared the turn's workspace while others still ran, and a ranged file still in its symbol lookup could resolve it again for the next prompt-less loop (summarize, resume). Now every attachment settles first.
  • An onboarding first-job mark uses the credential of the tool's own turn. The trigger site notes the turn's credential by call id for that call's plugins, and the hook claims under it, so a tenant switch mid-turn or a late return can't drop the other tenant's row. The plugin hook input is unchanged.
  • Tests: the first-events loop test runs with the workspace feature on, and the telemetry event list has first_prompt_sent and first_job_completed.
  • One comment no longer names a branch.
  • The workspace kill switch stops a saved link's first events. With ALTIMATE_DISABLE_WORKSPACE set, a project linked before the switch was set sends no first_prompt_sent or first_job_completed for that workspace, and an onboarding session's own rows name no workspace. Nothing is marked, so the workspace's first rows are sent once the switch is off. Other rows still name the link, as before; the docs say so.

How did you verify your code works?

Each fix's new test fails without it: a missing directory with a slower ranged file, a late return after a tenant switch, a switch mid-turn in a real loop, and the kill switch in a linked turn, an onboarding turn and the onboarding hook. With the feature on, the first-events loop file passes 15/15 and the pinned and turn-boundary files 15/15; each logs one unhandled "All fibers interrupted" error, as on main. test/altimate/telemetry, workspace and plugin pass (1,622 tests). Typecheck and Marker Guard pass.

Cost side effects

None. A failed prompt waits for its other attachments before it reports the error.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code

https://claude.ai/code/session_01VEmKQswEEpkYFmMEngESx8


Summary by cubic

Fixes a bug where a failed prompt could leave a stale workspace verdict for the next loop to take, and makes the first-job mark use the credential of the turn that ran the tool.

Bug Fixes

  • Now every attachment settles before a failure is rethrown; a new test in pinned-workspace-loop.test.ts covers the race.
  • The first-job mark now claims under the turn's credential, noted per call id, so a tenant switch mid-turn or a late return doesn't drop the other tenant's row.

Follow-ups from the workspace telemetry review

  • With ALTIMATE_DISABLE_WORKSPACE set, no first event names the saved link: a project linked before the switch was set sends no first_prompt_sent or first_job_completed, onboarding rows name no workspace, and nothing is marked, so the workspace's first rows are sent once the switch is off. All other rows still name the link.
  • The first-events loop test runs with the workspace feature on and clears any pin from the runner's environment.
  • The telemetry event-type list adds both first-event types.

Written for commit 88756ea. Summary will update on new commits.

View guided diff Turn on auto-fix

Summary by CodeRabbit

  • Bug Fixes

    • Workspace attribution for onboarding completion events now follows the workspace resolved for the tool call, avoiding incorrect attribution when a project link differs from the active workspace.
    • Disabling workspace features now suppresses workspace first-event telemetry and removes workspace attribution from onboarding-session rows. Other turn telemetry continues to include the cached project link.
    • Prompt processing now waits for all parts to finish before reporting a failure, helping prevent incomplete workspace binding after an error.
  • Tests

    • Expanded coverage for workspace attribution, telemetry suppression, and prompt failure scenarios.

ralphstodomingo and others added 2 commits November 11, 2026 17:36
…st gaps

- Every attachment part settles before a failed one is rethrown, so a late part can't put back the cleared verdict. Tested.
- The first-events loop test runs with the workspace feature on.
- The telemetry event list has both first-event types.
- A comment no longer names a branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEmKQswEEpkYFmMEngESx8
…link

States what the CLI already does: a cached link still names the turn's rows and still sends the workspace's first events once. No code change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEmKQswEEpkYFmMEngESx8
@ralphstodomingo ralphstodomingo self-assigned this Nov 11, 2026
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The change filters workspace first-event telemetry when the workspace kill switch is set and records call-scoped credentials for onboarding completion claims. Prompt input-part processing now waits for all operations to settle before propagating a rejection. Tests and telemetry documentation cover these behaviors.

Changes: Workspace telemetry and prompt processing

Layer / File(s) Summary
Workspace first-event policy
packages/opencode/src/altimate/workspace/first-events.ts, packages/opencode/src/altimate/workspace/state.ts, docs/docs/reference/telemetry.md, packages/opencode/test/session/workspace-first-events-loop.test.ts, packages/opencode/test/telemetry/telemetry.test.ts
First-event functions filter workspace IDs through the kill switch. Tests and documentation cover event suppression, workspace attribution for other turn rows, and event-type completeness.
Prompt call scope and settlement
packages/opencode/src/session/prompt.ts, packages/opencode/test/session/pinned-workspace-loop.test.ts, packages/opencode/test/session/workspace-first-events-loop.test.ts
Prompt tool execution records the bound workspace scope for each call. Input-part processing waits for all operations to settle before propagating a rejection. Tests cover call scope and a failed prompt with delayed attachment processing.
Onboarding completion attribution
packages/opencode/src/altimate/plugin/onboarding-telemetry.ts, packages/opencode/test/altimate/telemetry/onboarding.test.ts
Onboarding completion resolves the event workspace through the kill-switch filter and uses the tool call’s recorded scope when claiming the first-job event. Tests cover tenant scope and disabled-workspace behavior.

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

Change: Bug fix

Possibly related PRs

Suggested reviewers: sahrizvi, anandgupta42

Merge Risk: Minimal · up to 88756

The change makes workspace first-event telemetry respect the kill switch, ties the first-job mark to the originating tool call's credential, and waits for attachments to settle before a failed prompt returns. No actionable merge-blocking risk remains after review.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names a real part of the change: a failed prompt now waits for all attachments before reporting an error, so it leaves no stale workspace verdict. It also names the follow-up review fixes, w…
Description check Passed The description follows the template. It has the issue link, type of change, a detailed 'What does this PR do?' section, verification steps, and a cost side effects section. It is complete, and the ch…
Linked Issues check Passed The only directly linked issue, #1698, is closed and completed. Under the assessment rules, it supplies background only, so no active linked-issue coding requirements apply. The PR's stated objectives…
Out of Scope Changes check Passed All changes fall within the workspace telemetry and workspace-binding area that the PR states it addresses. Changes include: (1) Promise.allSettled in packages/opencode/src/session/prompt.ts so a fail…
Full details: Title check

Explanation

The title names a real part of the change: a failed prompt now waits for all attachments before reporting an error, so it leaves no stale workspace verdict. It also names the follow-up review fixes, which the description covers. It is broad because the PR also covers the kill switch, the tenant-scoped first-job mark, and test changes, but none of it is off-topic.

Full details: Description check

Explanation

The description follows the template. It has the issue link, type of change, a detailed 'What does this PR do?' section, verification steps, and a cost side effects section. It is complete, and the checklist is marked. One minor point: the template asks for a closed issue reference and a 'Documentation' box is checked, which matches the docs change.

Full details: Linked Issues check

Explanation

The only directly linked issue, #1698, is closed and completed. Under the assessment rules, it supplies background only, so no active linked-issue coding requirements apply. The PR's stated objectives are not checked against #1698's text as requirements. The PR also says it closes #1698, but this does not create an open requirement.

Full details: Out of Scope Changes check

Explanation

All changes fall within the workspace telemetry and workspace-binding area that the PR states it addresses. Changes include: (1) Promise.allSettled in packages/opencode/src/session/prompt.ts so a failed prompt waits for other attachments before it rethrows, which prevents a stale workspace verdict; (2) per-call credential scope in packages/opencode/src/altimate/workspace/first-events.ts and its use in packages/opencode/src/altimate/plugin/onboarding-telemetry.ts; (3) the ALTIMATE_DISABLE_WORKSPACE filter in firstEventWorkspace, promptSent, and jobCompleted; (4) tests for these paths; (5) adding first_prompt_sent and first_job_completed to the telemetry event-type list; (6) a docs update for the kill-switch behavior; (7) a comment wording change in workspace/state.ts. Each change connects to a stated objective. The evidence is the PR summary and the change summaries. I did not inspect the source diff, so the connection rests on those summaries. No change was found that is clearly unrelated.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

A rabbit checks the workspace gate,
Then tracks each call and scope.
First events wait for the right turn,
While settled tasks complete.
The burrow logs the rows it needs.

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4697acfd-227a-4247-ab6c-f22ae5e62461
Commits

Reviewing files that changed from the base of the PR and between 13a2a01 and 88756ea.

Files selected for processing: 9

  • docs/docs/reference/telemetry.md
  • packages/opencode/src/altimate/plugin/onboarding-telemetry.ts
  • packages/opencode/src/altimate/workspace/first-events.ts
  • packages/opencode/src/altimate/workspace/state.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/altimate/telemetry/onboarding.test.ts
  • packages/opencode/test/session/pinned-workspace-loop.test.ts
  • packages/opencode/test/session/workspace-first-events-loop.test.ts
  • packages/opencode/test/telemetry/telemetry.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

@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.

…wn turn

The trigger site notes the turn's credential under the call id while that call's plugins run, and the onboarding hook claims under it. A tenant switch mid-turn, or a late return, no longer drops the other tenant's row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEmKQswEEpkYFmMEngESx8
…ject's link

A project linked before the switch was set still sent that workspace's first_prompt_sent and first_job_completed. Now neither is sent, onboarding rows name no workspace, and nothing is marked, so the rows come once the switch is off. Other rows still name the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEmKQswEEpkYFmMEngESx8
@ralphstodomingo
ralphstodomingo marked this pull request as ready for review November 12, 2026 06:51
@ralphstodomingo

ralphstodomingo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Review log: claims and disclosed residuals (head 88756ea)

Review against these. A finding is a reproducible trace that violates a numbered claim.

Claims

  • C1, the verdict: when a prompt fails on an attachment, every part settles before the failure is rethrown (settled, packages/opencode/src/session/prompt.ts:3212, used at :3273 and :3605). So no attachment read still running sets a turn verdict (turnBinding) for the session after prompt has ended it, and the next loop started without a prompt (summarize, resume) resolves its own workspace.
  • C2, the per-call tenant: an onboarding first_job_completed mark (packages/opencode/src/altimate/plugin/onboarding-telemetry.ts:268-281) is claimed under the credential scope of the turn that ran the tool. The trigger site notes that scope by call id (prompt.ts:2983, packages/opencode/src/altimate/workspace/first-events.ts:95-108), and the claim doesn't read the credential at claim time. A credential switch mid-turn, or a tool returning after its turn was cancelled and replaced, can't mark another tenant's workspace or leave its own unmarked.
  • C3, the kill switch: with ALTIMATE_DISABLE_WORKSPACE set, no first_prompt_sent or first_job_completed row carries a workspace_id, and no first-event mark is written (first-events.ts:112-140, onboarding-telemetry.ts:268). An onboarding session's own rows are still sent, without workspace_id. With the switch unset, which first-event rows are sent and what they carry is unchanged from main.

Disclosed residuals (not findings)

  • R1: with the switch set, the turn's other rows (session_start, task_outcome_signal and the rest) still name the project's saved link, read from the local cache. This is the decided behaviour, and the telemetry docs say so.
  • R2: the switch is read at each first-event decision, so setting it mid-session applies from the next prompt or turn end.
  • R3: the switch doesn't clear marks written, or rows sent, before it was set.
  • R4: the loop test files log one unhandled "All fibers interrupted" error at teardown, as on main.
  • R5 (cubic, 06:54Z): C2 assumes tool call ids are unique among calls running at the same time in one process. A provider that repeats an id across two overlapping calls whose turns ran under different credentials could have one onboarding sample-setup mark take the other's tenant. Provider ids pass through unchanged, and the AI SDK makes them unique.
  • R6 (Kilo, 07:12Z): LSP.documentSymbol has no timeout, so a hung language server stalls a prompt with a ranged attachment whose range came back empty, as on main. With C1, a failing sibling part also waits for it.
  • R7 (Kilo): only the native tool path notes a call scope. No other path can complete an onboarding job today: the MCP and task triggers can't reach isJobCompletion as true.
  • R8 (Kilo): the verdict test orders its parts with a 300 ms timer. On a slow host it can pass without the fix, but it can't fail with it.

@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review against the numbered claims and the disclosed residuals in the review-log comment on this PR: report only a reproducible trace that violates a numbered claim. Instances of the disclosed residuals are not findings. A round with no claim violation ends review.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 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-12T06:54:37.857889Z 88756ea Manual request
ℹ️ 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 88756ea316

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@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.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/altimate/workspace/first-events.ts">

<violation number="1" location="packages/opencode/src/altimate/workspace/first-events.ts:92">
P2: `callScopes` overwrites an earlier scope whenever overlapping tool executions reuse the same `callID`, so a late `tool.execute.after` can claim the workspace under the replacement turn's tenant or fall back to the current tenant. Key the scope by an execution-unique token (or retain per-call entries) rather than relying on provider call IDs being unique across concurrent turns.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

* call's workspace id belongs to, even when the credential has switched since, or the turn was
* cancelled and replaced before the tool returned. Keyed by call id, not session: a session's later
* turn has its own calls. */
const callScopes = new Map<string, Scope>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: callScopes overwrites an earlier scope whenever overlapping tool executions reuse the same callID, so a late tool.execute.after can claim the workspace under the replacement turn's tenant or fall back to the current tenant. Key the scope by an execution-unique token (or retain per-call entries) rather than relying on provider call IDs being unique across concurrent turns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/workspace/first-events.ts, line 92:

<comment>`callScopes` overwrites an earlier scope whenever overlapping tool executions reuse the same `callID`, so a late `tool.execute.after` can claim the workspace under the replacement turn's tenant or fall back to the current tenant. Key the scope by an execution-unique token (or retain per-call entries) rather than relying on provider call IDs being unique across concurrent turns.</comment>

<file context>
@@ -80,8 +84,38 @@ export async function claim(event: FirstEvent, workspaceId: number, scope?: Scop
+ * call's workspace id belongs to, even when the credential has switched since, or the turn was
+ * cancelled and replaced before the tool returned. Keyed by call id, not session: a session's later
+ * turn has its own calls. */
+const callScopes = new Map<string, Scope>()
+
+/** Notes `scope` for `callID` until the returned handle is disposed. */
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declined for this PR, recorded as R5 on the review log.

The overwrite needs two tool calls running at the same time in one process with the same call id. The id is the provider's toolCallId. sanitizeToolCallID replaces only a missing or non-string one, and the AI SDK providers return or generate unique ids. Even with a repeated id, the wrong tenant is taken only when the two calls' turns ran under different credentials, and only for an onboarding sample-setup completion. With the same credential, the fallback to the current credential names the same tenant.

Keying by something unique to each execution would mean adding it to the plugin hook input, which this PR keeps unchanged, or carrying it in async context. Either is a bigger change than this case warrants before this release.


const parts = await Promise.all(
// altimate_change start — every part settles before a failure is rethrown (see `settled`)
const parts = await Promise.allSettled(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: A failed prompt now waits for every attachment, so an attachment that never settles withholds the error indefinitely.

Promise.allSettled removes the fail-fast behaviour: createUserMessage no longer rejects until every part settles. The ranged text/plain attachment part awaits LSP.documentSymbol (packages/opencode/src/session/prompt.ts:3397), which runs client.connection.sendRequest("textDocument/documentSymbol", ...) with no timeout (packages/opencode/src/lsp/lsp.ts:301-304,427-433); the .catch(() => []) there only handles rejection, not a promise that never settles, and ctx.abort for these reads is a fresh never-aborted AbortController (prompt.ts:3435). So with one failing sibling part (e.g. the missing-directory read) and a hung LSP server or unresponsive language server, the failure used to surface immediately and now never surfaces: the session stays busy with no error and no way to know the prompt failed.

Suggestion: keep waiting for the parts that settle, but bound the wait (per-part timeout, or race the settled set against a deadline) so the original error is still reported when a sibling hangs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declined for this PR, recorded as R6 on the review log.

The hang comes from LSP.documentSymbol having no timeout, and it is on main too. When no sibling part fails, Promise.all on main also waits for that symbol lookup, so a hung language server stalls every prompt with a ranged attachment whose range came back empty. This PR changes only the case where another part also fails: the error now waits for the hung part as well, instead of surfacing first. Bounding the lookup belongs on documentSymbol itself, which fixes both cases, and is its own change.

await WorkspaceFirstEvents.claim(
"first_job_completed",
workspaceId,
WorkspaceFirstEvents.callScope(input.callID),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: callScope(input.callID) is only populated on the native registry path, so this claim silently falls back to the credential at tool-return time on other paths.

WorkspaceFirstEvents.noteCallScope is called only in resolveTools's native tool wrapper (packages/opencode/src/session/prompt.ts:2983). The MCP wrapper also fires tool.execute.after (prompt.ts:3090-3100) and the task tool fires it too (prompt.ts:1420-1430), but neither notes a scope. The MCP loop assigns tools[key] = item after the native loop (prompt.ts:3163), so an MCP server exposing a tool named sample_setup replaces the native one and reaches this hook through the un-noted path. callScope is then undefined, and claim() falls back to currentScope() (packages/opencode/src/altimate/workspace/first-events.ts:53), which reads the credential now — the exact cross-tenant read this PR removes.

This is latent today because sample_setup is native, but the new mechanism's invariant is "the trigger site notes the scope". Noting it at the MCP (and task) trigger sites, or deriving the scope from the turn context rather than a single call site, keeps the invariant true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declined, recorded as R7 on the review log: not reachable for this mark.

Only sample_setup completes a job (isJobCompletion, onboarding-telemetry.ts:183), and only the native tool reaches it by that name. The task trigger isn't a job (jobForTool maps no task). An MCP tool's key is <server>_<tool> (mcp/index.ts:1251), so it would take a server named sample with a tool named setup. Even then the MCP trigger passes the raw call result, which carries no metadata.success, so isJobCompletion is false and nothing is claimed. If a job-completing tool is ever added on another path, its trigger site should note the scope as the native one does.

accountSees([{ id: LINKED, name: "project-link" }])
yield* linkProject(dir)
// The ranged file's symbol lookup outlasts the missing directory, whose failed read fails the prompt.
symbolLookup = () => new Promise((done) => setTimeout(() => done([]), 300))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: The 300 ms timer is the only ordering guarantee, so on a slow host this regression test can pass without the production fix.

The test exists to prove a part that finishes after the failure cannot leave a turn verdict behind. If the missing-directory ReadTool takes longer than the 300 ms symbol lookup on a cold CI runner, the ranged part resolves its verdict first and the directory failure lands afterwards, so endTurnBinding is not followed by any late read and both the fixed and unfixed code reach the same RELINKED assertion.

packages/opencode/test/AGENTS.md asks for a published readiness signal instead of a fixed sleep. For example, have the failing part set a deferred/flag that symbolLookup awaits, so the late part provably settles after the failure rather than relying on relative timing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declined for this PR, recorded as R8 on the review log.

Agreed that the timer is the ordering guarantee. On a slow host the test can only pass without the fix; it can't fail with it, so it never blocks CI. It failed without the fix locally when it was written. Replacing the timer with a readiness signal is a test-only change worth making, and it can follow without touching the release.

@kilo-code-bot

kilo-code-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/session/prompt.ts 3273 Switching to Promise.allSettled means a failed prompt now waits for every attachment; the ranged text/plain part awaits LSP.documentSymbol, which has no timeout and only catches rejections, so a hung language server withholds the prompt's error indefinitely (session stays busy). Bound the wait while still letting parts settle.

SUGGESTION

File Line Issue
packages/opencode/src/altimate/plugin/onboarding-telemetry.ts 279 noteCallScope is only called on the native registry tool path; the MCP and task tool.execute.after triggers do not note one, and MCP tools replace native ones in the tools map, so a job-mapped MCP tool falls back to currentScope() at claim time — the cross-tenant read this PR removes.
packages/opencode/test/session/pinned-workspace-loop.test.ts 372 The 300 ms timer is the test's only ordering guarantee; on a slow CI host the ranged part can finish before the failing directory read, letting the regression test pass without the production fix. Use a readiness signal instead of a fixed sleep.

Note: the callScopes call-id collision raised by an existing inline comment (first-events.ts:92) was reviewed and not re-raised, since that thread is already active.

Files Reviewed (9 files)
  • docs/docs/reference/telemetry.md - no issues
  • packages/opencode/src/altimate/plugin/onboarding-telemetry.ts - 1 issue
  • packages/opencode/src/altimate/workspace/first-events.ts - no new issues (existing thread at line 92)
  • packages/opencode/src/altimate/workspace/state.ts - no issues (comment only)
  • packages/opencode/src/session/prompt.ts - 1 issue
  • packages/opencode/test/altimate/telemetry/onboarding.test.ts - no issues
  • packages/opencode/test/session/pinned-workspace-loop.test.ts - 1 issue
  • packages/opencode/test/session/workspace-first-events-loop.test.ts - no new issues
  • packages/opencode/test/telemetry/telemetry.test.ts - no issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 142.5K · Output: 40.5K · Cached: 3.7M

Review guidance: REVIEW.md from base branch main (truncated)

@sahrizvi sahrizvi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consensus review (Claude lead + Codex, both rounds completed): APPROVE

All three claims hold for ordinary inputs, and three of the four earlier minors are fixed. M1 (the onboarding first-job credential) is partly fixed: it is correct for distinct call ids, and minors 2 and 4 cover what is left. Every fix has a test that fails when the fix is reverted (10 of 10 mutations caught). test/session, test/altimate/{telemetry,workspace,plugin}, test/plugin and test/telemetry pass at head and at base, typecheck passes, and every prompt.ts edit sits inside altimate_change markers. The "All fibers interrupted" unhandled error also appears on base.

No critical or major finding is open. The remaining minors are inline, and none blocks:

  1. A failed prompt never reports its error when a sibling's symbol lookup hangs (prompt.ts:3273): an edge input on a hang that already exists on main.
  2. A repeated raw call id can swap or drop the noted tenant (first-events.ts:97): an edge input, safe in a follow-up.
  3. The error reported is the first failure in input order, not in time (prompt.ts:3213): cosmetic.
  4. A hook that never settles keeps its call-scope entry (prompt.ts:2983): an edge input.
  5. The verdict test's slow-lookup seam is never called (pinned-workspace-loop.test.ts:372): test-only.

Codex first rated minor 1 as major, then revised it to minor after a probe showed main hangs on the same input. Nits are in a separate comment.


const parts = await Promise.all(
// altimate_change start — every part settles before a failure is rethrown (see `settled`)
const parts = await Promise.allSettled(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Codex; reproduced by the lead). A failed prompt now waits for every sibling. If a ranged ?start=N&end=N file's LSP.documentSymbol never answers (no timeout, R6), the error is never reported.

Probe: a missing directory plus missing.txt?start=3&end=3, with the legacy LSP.documentSymbol spied to return a promise that never settles. Head was still pending after 8 s; base rejected in 537 ms.

Not a blocker: on main the same hung lookup already stalls every prompt with that attachment, and no loop starts. Safe in a follow-up: bound documentSymbol, or report the first rejection and clear the verdict once every part has settled.

// otherwise read the turn's workspace once `prompt` has cleared it, and leave a verdict for the next
// loop started without a prompt (summarize, resume) to take.
function settled<T>(results: PromiseSettledResult<T>[]): T[] {
const failed = results.find((result): result is PromiseRejectedResult => result.status === "rejected")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Codex). find picks the first rejected part in input order. Promise.all reported the first rejection in time. Example: a missing directory first and a malformed URL second. Main shows the URL error; head waits and shows the directory error. This only changes which diagnostic is shown, so it's cosmetic, but the PR's "error is still the first failure" doesn't hold literally.

/** Notes `scope` for `callID` until the returned handle is disposed. */
export function noteCallScope(callID: string | undefined, scope: Scope | undefined): Disposable {
if (callID === undefined || !scope) return { [Symbol.dispose]() {} }
callScopes.set(callID, scope)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Codex). The key is the raw options.toolCallId (prompt.ts:2870), and processor.ts:158 says a provider may repeat one malformed raw id for concurrent calls. So R5's "the AI SDK makes them unique" doesn't hold for this repo.

When two overlapping calls share an id under different turn credentials, the second set overwrites the first. If the second call disposes first, the first call's lookup returns undefined and claim falls back to the claim-time credential.

This is an edge input that costs one first-event attribution. Safe in a follow-up, for example by keying on the processor's execution identity.

// badge. Shared with SessionTools.resolve (session/tools.ts) so the resolvers can't drift.
const stamped = stampRegistryToolSource(output, item)
// The turn's credential, for this call's plugins: a first-event mark names its tenant.
using _callScope = WorkspaceFirstEvents.noteCallScope(ctx.callID, input.workspaceScope)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Codex). Disposal happens only when this block exits. A tool.execute.after hook that never settles keeps its entry, because cancelling the session doesn't unwind a pending hook. That leaves one entry per hung hook, with no hard cap. It's an edge input: a hung hook already hangs this call on main. Success and thrown-error exits dispose correctly.

accountSees([{ id: LINKED, name: "project-link" }])
yield* linkProject(dir)
// The ranged file's symbol lookup outlasts the missing directory, whose failed read fails the prompt.
symbolLookup = () => new Promise((done) => setTimeout(() => done([]), 300))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor, test-only (lead; Codex agreed). This seam is never called. The stub replaces documentSymbol on LSP.Service (@/lsp/lsp, line 83), but prompt.ts:74 imports LSP from ../lsp, the legacy namespace. An instrumented symbolLookup never printed.

The test still fails when allSettled is reverted to Promise.all (3 of 3 runs), but through natural ordering, not this timer. So the comment above and R8 are inaccurate, and nothing tests a slow or hung lookup. Spying on the namespace (spyOn(LSP, "documentSymbol") from src/lsp) would make the ordering explicit.

@sahrizvi

Copy link
Copy Markdown
Collaborator

Consensus review nits (Claude + Codex), not blocking

  1. No test for call-scope cleanup. Making noteCallScope's dispose a no-op leaves every test green. The onboarding "late return" test also calls resetForTest() before its using handles leave scope, which would hide a missing delete.
  2. The ALL_EVENT_TYPES check is a count. Reverting the new names together with the count stays green, and nothing checks that the list covers the whole Telemetry.Event union.
  3. Marker guard. It reports "ok" because none of the changed paths exist in upstream/dev. Checked by hand instead: every prompt.ts edit is inside altimate_change blocks, and telemetry.test.ts is fork-only, so it needs none.

This branch has not been deployed

No deployments
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.

A workspace's first prompt and first finished job aren't sent outside onboarding

2 participants