Skip to content

refactor(coding-agent): derive available connection models on read - #1826

Closed
snimu wants to merge 124 commits into
mainfrom
snimu/derive-connection-models
Closed

refactor(coding-agent): derive available connection models on read#1826
snimu wants to merge 124 commits into
mainfrom
snimu/derive-connection-models

Conversation

@snimu

@snimu snimu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The TUI stored connectionModels — a filtered copy of the model catalog x configured providers — with its own assignment, clearing, and versioned return paths. A pure function of two other fields, stored, and therefore synchronizable and driftable (audit: duplicate sources of truth, dup-truth.md finding 4).

The fix

The stored copy and its sync paths are deleted; the available-model list derives on read with the identical predicate (catalog.filter(m => providers.has(m.provider))). The catalog, provider set, and async-refresh generation guard remain — they are source data and stale-request protection, not copy-sync. +17/−22.

How it's verified

Reviewer confirmed all 4 former field sites converted (zero references remain), predicate character-identical including the invalidate and stale-generation cases, derivation off any render hot path, and the retained machinery guards source data only. 174/174 focused; full CI-style suite identical failing set to stack base. Two-model implement/review loop, approved first pass.

Stacked on #1739 (test the whole stack at the leaf; merge base-first).

Note: intentionally no Linear ticket for this cleanup stack, so that check stays red.


Note

Medium Risk
Touches compaction continuation, cross-daemon messaging, RPC transport semantics, and supervised renames—core loop behavior with behavior changes if edge cases (cancellation during queued-work wait, long RPC calls) were wrong.

Overview
Interactive TUI stops mirroring the model catalog and message queue in local fields: available models and queued steering/follow-ups are derived from connectionState on read, and “new chat” hints use message count instead of a separate sessionHasMessages flag. Queue browsing/editing reconciles selection against server state via QueueSelection.refreshAt rather than optimistic local patches.

Agent session rewrites post-compaction continuation to run immediately (no 100ms timer), waiting on idle/retry/refine, queued-work pauses, compaction, and session-input pumps before calling agent.continue(). RLM child roster snapshots are centralized on AgentSession (duration, preview, tools, activity, recap) with retained children stored as { session, run? }; daemon attach builds child snapshots by delegating to getRlmChildSnapshots() plus resident active-session ids.

Daemon / RPC: remote agent messages connect to the supervisor once, send once, and do not resend if the socket dies after delivery; worker-mode renames apply supervisor-approved names without re-running availability checks. Legacy RLM registries without sessionDir hydrate via shared readLegacyRlmSubagentRegistry (defaults dir from the session file). RpcClient drops fixed request timeouts and surfaces transport death via transportError, failing pending waits promptly.

Smaller cleanups: shared looksLikeSessionPath, kernel dispose snapshot after interrupting/waiting on execution queue, removal of test-only config-cache hooks and getOverflowPatterns, and telemetry no longer auto-off under NODE_ENV=test.

Reviewed by Cursor Bugbot for commit d34ad9b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Derive InteractiveMode connection queue and models from connectionState on read

  • Removes local mirror fields connectionQueue, sessionHasMessages, and connectionModels from InteractiveMode; queue contents, available models, and new-chat detection are now computed on demand from connectionState via getConnectionQueue(), getAvailableConnectionModels(), and connectionState.messageCount.
  • Replaces QueueSelection.sync with refreshAt(queue, lane, index, expectedText) — selection reconciliation is now strict to lane/index and expected text, no longer retargeting to duplicate text elsewhere.
  • Queue move/edit operations (moveQueueSelection, editQueueSelection) now defer to server state and reconcile selection post-events via pendingQueueMove gating, preventing double-apply and races.
  • Reworks RpcClient to track transportError and fail all pending operations via failPendingOperations; removes per-request client-side timeouts (REFINE_REQUEST_TIMEOUT_MS) so long-running commands are no longer cut off by the client.
  • Replaces timer-based post-compaction continuation scheduling in AgentSession with async coordination (_waitForQueuedWorkResume, _runScheduledPostCompactionContinue) that waits on agent/refine/queued-work idle states and honors a continueAfterSessionInput flag.
  • Reworks ReplKernelManager.flushSnapshotForDispose to interrupt active execution, wait up to SNAPSHOT_EXECUTION_TIMEOUT_MS for settle, then capture a bounded snapshot instead of racing snapshotState against a separate dispose timeout.
  • Introduces readLegacyRlmSubagentRegistry in rlm-ledger.ts for tolerant parsing of legacy rlm-subagents.jsonl files; daemon buildRlmChildSnapshots now sources snapshots from root.runtime.session.getRlmChildSnapshots() and overlays daemon activeSessionId.
  • Removes test-only exports: clearApiKeyCache from model-registry, clearConfigValueCache from resolve-config-value, getOverflowPatterns from overflow, and the NODE_ENV === "test" telemetry opt-out branch in isTelemetryEnabled.
  • Risk: RpcClient.send no longer enforces per-request timeouts — consumers relying on client-side timeout for long operations must pass explicit timeouts to waitForIdle/promptAndWait or implement higher-level timeout logic. QueueSelection.refreshAt drops selection when the item is not at the exact lane/index with matching text, which may surprise callers expecting text-based retargeting. findActiveDaemonSessionSummaryForInteractiveStartup is no longer exported from main.ts.

Macroscope summarized d34ad9b.

Linear ticket: ENG-5660
(ticket linked above)


Supersedes #1741 (recreated as a plain PR against main; GitHub's stack lock prevented retargeting the stacked PR).

snimu added 30 commits August 24, 2026 11:17
snimu added 21 commits August 27, 2026 10:22

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 63cbcd3. Configure here.

lane[selected.index] === selected.text &&
target >= 0 &&
target < lane.length
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Queue selection lost on reorder race

Medium Severity

Reordering a queued message can drop browse selection when the mutation response arrives before the matching session_action_update. moveQueueSelection awaits sessionEventQueue (already-queued events only), then refreshAt requires the new index to already be in connectionState. On a stale queue that check fails and resets the cursor, so the editor jumps back to the draft even though the move applied.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 63cbcd3. Configure here.

this.queuedMessagesContainer.clear();
this.connectionQueue = { steering: [], followUp: [] };
this.pendingQueueEdit = undefined;
this.pendingQueueMove = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Session reset keeps old queue images

Medium Severity

resetCurrentSessionRenderState no longer clears the session queue before pruning pasted images. liveImageMarkerIds now reads connectionState.sessionActions, so on paths such as renderCurrentSessionState the previous session's queued [image #N] markers still count as live and those blobs are not dropped.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 63cbcd3. Configure here.

}
this.subscribeToAgent();
await Promise.all([this.refreshConnectionQueue(), this.refreshHeartbeatCatalog().catch(() => undefined)]);
this.updatePendingMessagesDisplay();

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 interactive/interactive-mode.ts:2815

rebindCurrentSession and renderCurrentSessionState display and edit queued messages from a pre-subscription snapshot, so a session_action_update that arrives during either transition is missed and the UI retains stale queue state until another queue event or resync. Restore an authoritative queue read after subscribing (and after rendering a replacement snapshot) before updating the queued-message display.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/interactive-mode.ts around line 2815:

`rebindCurrentSession` and `renderCurrentSessionState` display and edit queued messages from a pre-subscription snapshot, so a `session_action_update` that arrives during either transition is missed and the UI retains stale queue state until another queue event or resync. Restore an authoritative queue read after subscribing (and after rendering a replacement snapshot) before updating the queued-message display.

Evidence trail:
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2891-2899, 5087-5124, 5327-5375, 6569-6584 at 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; git_diff MERGE_BASE REVIEWED_COMMIT -- packages/coding-agent/src/modes/interactive/interactive-mode.ts

private async _runScheduledPostCompactionContinue(settlement: PostCompactionContinuationSettlement): Promise<void> {
while (this._postCompactionContinuationScheduled && this._postCompactionContinuationSettlement === settlement) {
await this.agent.waitForIdle();
await this.waitForRetry();

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 core/agent-session.ts:7658

Overflow recovery stalls permanently because _runScheduledPostCompactionContinue waits on waitForRetry() before calling agent.continue(), while that retry promise is only resolved after the retry starts and finishes. Remove this wait so the scheduled continuation can issue the retry.

			await this.agent.waitForIdle();
-			await this.waitForRetry();
			await this._waitForRefineIdle();
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 7658:

Overflow recovery stalls permanently because `_runScheduledPostCompactionContinue` waits on `waitForRetry()` before calling `agent.continue()`, while that retry promise is only resolved after the retry starts and finishes. Remove this wait so the scheduled continuation can issue the retry.

Evidence trail:
packages/coding-agent/src/core/agent-session.ts:7655-7696 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; packages/coding-agent/src/core/agent-session.ts:10681-10686 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; packages/coding-agent/src/core/agent-session.ts:8408-8435 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; packages/coding-agent/src/core/agent-session.ts:8592-8601 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; packages/coding-agent/src/core/agent-session.ts:3667-3696 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d; packages/coding-agent/src/core/agent-session.ts:10920-10924,10947-10954 @ 63cbcd3489d2e6c857b0a63d076e23834bac3b3d

/** Best-effort final snapshot before a graceful dispose, bounded by a timeout. */
private async flushSnapshotForDispose(): Promise<void> {
if (!this.options.snapshot || !this.isRunning) return;
const pendingExecutions = this.executionQueue;

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 kernel/repl-manager.ts:1104

dispose() and shutdown({ snapshot: true }) can wait indefinitely instead of honoring SNAPSHOT_EXECUTION_TIMEOUT_MS. flushSnapshotForDispose() captures executionQueue before waiting, so a concurrent execute() can enqueue a non-terminating request afterward; captureSnapshot() queues behind it, and its timeout does not start until that request finishes. Reserve the queue before awaiting it (or otherwise block new executions) so the final snapshot cannot be overtaken.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/kernel/repl-manager.ts around line 1104:

`dispose()` and `shutdown({ snapshot: true })` can wait indefinitely instead of honoring `SNAPSHOT_EXECUTION_TIMEOUT_MS`. `flushSnapshotForDispose()` captures `executionQueue` before waiting, so a concurrent `execute()` can enqueue a non-terminating request afterward; `captureSnapshot()` queues behind it, and its timeout does not start until that request finishes. Reserve the queue before awaiting it (or otherwise block new executions) so the final snapshot cannot be overtaken.

Evidence trail:
packages/coding-agent/src/core/kernel/repl-manager.ts:434-450, 453-497, 897-910, 1002-1020, 1102-1117 at 63cbcd3489d2e6c857b0a63d076e23834bac3b3d

@snimu

snimu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the chained stack was restructured into independent PRs (byte-identical combined tree). A fresh standalone PR for this change follows on the same branch name.

@snimu snimu closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant