Skip to content

fix: Issues-tab status writeback (409/scope) + terminal reconciler silent-death modes#226

Merged
khaliqgant merged 6 commits into
mainfrom
fix/issues-writeback-revision-and-reconciler-gates
Jun 11, 2026
Merged

fix: Issues-tab status writeback (409/scope) + terminal reconciler silent-death modes#226
khaliqgant merged 6 commits into
mainfrom
fix/issues-writeback-revision-and-reconciler-gates

Conversation

@khaliqgant

Copy link
Copy Markdown
Member

Summary

Two fix sets from today's debugging, verified independently of the in-flight cloud-auth work (built and tested from a clean main worktree).

1. Issues tab: changing a Linear issue status always failed

  • 409 revision_conflict: writeRemoteFile hardcoded baseRevision: '0' ("file must not exist") — correct for Slack writeback creates, wrong for the Issues tab's PATCH writeback onto the existing canonical issue file. It now reads the file's current revision first (keeping '0' only when the read 404s) and retries exactly once on a conflict; a second consecutive conflict propagates. The 429 workspace_busy errors were fallout from retrying the doomed write.
  • list-remote-dir scope rejection on every Issues load: the status dropdown loads /linear/states, but Linear integrations configure mount scopes like /linear/issues — the states subtree is reference data, not a configurable mount, so the scope check rejected it and the UI silently fell back to issue-derived states. Added a read-only carve-out (isLinearStatesListablePath, mirroring the Slack-DM one): list/read allowed whenever a Linear integration is visible in the project; writes remain rejected.

2. Terminal reconciler: three silent-death modes closed

The quiet-time convergence backstop (broker snapshot reconciler) could be disabled silently, exactly while corruption was visible:

  • Persistent PTY/grid dims mismatch skipped every check forever with no log — and that mismatch is itself the state that creates stacked-frame corruption. Now escalates after 2 consecutive quiet checks: invalidates the size-sync ack, refits, and re-asserts the rendered grid as the PTY size (logged, 30s rate limit).
  • Stranded optimistic-echo predictions held the quiet gate shut permanently (the engine has no time-based expiry; a keystroke the TUI swallows never confirms). Rolled back after 10s of output silence — only optimistic glyphs are erased; confirmed bytes untouched.
  • A rejecting snapshot IPC was an unhandled rejection every 4s cycle; the poll loop now catches and rate-limit-logs.

AGENTS.md "Terminal Screen Convergence" documents the new invariants.

Test plan

  • vitest: integrations (52), integration-mounts (36), terminal-reconciler (incl. 4 new gate tests), terminal-runtime-registry.dom, echo-router, pty-size-sync — all pass in the clean worktree
  • npm run typecheck (web + node) clean
  • Manual: restart Pear, change a Linear issue status in the Issues tab → expect the write to land (file briefly shows the sparse {stateId} payload until Linear's webhook re-materializes the record); no [cloud-auth-diag] listRemoteDirectory REJECT for /linear/states in the main-process log

🤖 Generated with Claude Code

khaliqgant and others added 2 commits June 11, 2026 14:14
The quiet-time screen reconciler (the convergence backstop for renderer/
broker divergence) had three ways to be disabled silently, exactly while
corruption was on screen:

- A persistent PTY/grid dims mismatch — the very state that creates
  stacked-frame corruption — skipped every check with no log, forever.
  Now escalates after 2 consecutive quiet checks via
  onPersistentDimsMismatch: invalidate the size-sync ack, refit, and
  re-assert the rendered grid as the PTY size (30s rate limit).
- A stranded optimistic-echo prediction held the quiet gate shut
  permanently: the engine has no time-based expiry, so a keystroke the
  TUI swallows (or typed into a hung agent) wedges hasPredictions true.
  Now rolled back after 10s of output silence; only optimistic glyphs
  are erased, confirmed bytes untouched.
- A rejecting snapshot IPC was an unhandled rejection per 4s cycle; the
  check loop now catches and rate-limit-logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-only /linear/states

Changing a Linear issue status from the Issues tab always failed with a
409 revision conflict: writeRemoteFile hardcoded baseRevision '0'
("file must not exist"), which is right for Slack writeback creates but
wrong for PATCH writebacks onto existing canonical files. Now reads the
file's current revision first ('0' only when the read 404s) and retries
exactly once if a concurrent sync bumps the revision mid-write.

The Issues tab also lists /linear/states for the status dropdown, which
the mount-scope check rejected on every load (integrations configure
mounts like /linear/issues; the states subtree is reference data, not a
configurable mount). Add a read-only carve-out mirroring the Slack DM
one: list/read allowed when a Linear integration is visible, writes
still rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b54fb4f2-6f84-4327-93d8-4b652f485235

📥 Commits

Reviewing files that changed from the base of the PR and between 6cbb229 and 6bda881.

📒 Files selected for processing (5)
  • src/main/__tests__/integration-event-bridge.test.ts
  • src/main/integrations.test.ts
  • src/main/integrations.ts
  • src/renderer/src/lib/terminal-reconciler.test.ts
  • src/renderer/src/lib/terminal-reconciler.ts

📝 Walkthrough

Walkthrough

This PR harddens the terminal reconciliation system against persistent dimension mismatches and stranded predictions, while introducing Linear integration remote-filesystem support with optimistic-concurrency write semantics and revision conflict recovery.

Changes

Linear Integration Remote File Access & Optimistic-Concurrency Writes

Layer / File(s) Summary
Linear States Path Recognition & Listing Scope Control
src/main/integration-remote-paths.ts, src/main/integrations.ts
Introduces isLinearStatesListablePath predicate to recognize /linear/states paths, gates listing/read access via linearStatesListingEnabledForProject, and extends both readRemoteFile scope checks and listRemoteDirectory scope gating/filtering to permit Linear states access when the gate is enabled.
Optimistic-Concurrency Write Logic with BaseRevision & Retry
src/main/integrations.ts
Replaces unconditional baseRevision: '0' with revision-aware flow: reads existing file revision (ignoring 404), writes using that baseRevision, and retries once on HTTP 409 conflict with a fresh read. Writer workspace scopes expanded to include both read and write permissions.
Integration Tests – Linear Access & Write Behavior Validation
src/main/integrations.test.ts
Mocks relayClient.listTree for directory-listing tests; validates Linear states read/list rules (allowed only with visible Linear integration, no relayClient.listTree call when disabled); tests write flow (read-then-write with correct baseRevision, 404 creates with '0', 409 conflict retries exactly once, second conflicts propagate); ensures writes to /linear/states are rejected.

Terminal Screen Convergence Hardening

Layer / File(s) Summary
Persistent Dimension Mismatch Detection & Escalation
src/renderer/src/lib/terminal-reconciler.ts, src/renderer/src/lib/terminal-reconciler.test.ts
Adds RECONCILE_DIMS_MISMATCH_CHECKS, RECONCILE_DIMS_KICK_GAP_MS, RECONCILE_ERROR_LOG_GAP_MS constants and extends TerminalReconcilerDeps with optional onPersistentDimsMismatch callback. Tracks consecutive quiet-mode dimension mismatches, escalates when streak exceeds threshold and rate limit permits, aborts divergence/repair work until dimensions stabilize. Tests verify mismatch detection timing, escalation rate-limiting, repair prevention, and streak reset.
Reconciliation Error Handling & Stranded Prediction Recovery
src/renderer/src/lib/terminal-reconciler.ts, src/renderer/src/lib/terminal-runtime-registry.ts, src/renderer/src/lib/terminal-reconciler.test.ts
Wraps check() interval with error handling that catches and rate-limits failures without terminating the loop. Adds STRANDED_PREDICTION_ROLLBACK_MS timeout; after stranded window elapses, triggers predictiveEcho.rollback() to unlock the quiet gate. When onPersistentDimsMismatch fires, invalidates PTY size-sync ack, refits, updates predictive echo, and sets desired PTY dimensions. Tests verify error handling, log rate-limiting, continued reconciliation, and PTY recovery.

Documentation & Test Synchronization

Layer / File(s) Summary
Terminal Reconciler Hardening Requirements & Test Synchronization
AGENTS.md, src/main/__tests__/integration-event-bridge.test.ts
Updates AGENTS.md with terminal reconciler failure-mode guarantees (persistent dimension escalation, stranded prediction rollback, error rate-limiting). Adds explicit synchronization point in integration-event-bridge test to ensure async injection completes before assertion.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AgentWorkforce/pear#222: Both PRs modify IntegrationsManager.writeRemoteFile pipeline—main PR changes baseRevision handling and conflict retry while related PR adds the IPC/preload surface for writeRemoteFile.
  • AgentWorkforce/pear#197: Both PRs modify IntegrationsManager.readRemoteFile in src/main/integrations.ts—main PR extends scope gating for Linear states reads while related PR changes 404 handling to return a { kind: 'missing' } preview.
  • AgentWorkforce/pear#221: Both PRs harden terminal pipeline by tying reconciliation/runtime behavior to PTY size-sync state and predictive-echo transitions via dimension-kick/rollback logic and foundational pty-size-sync/echo-router changes.

Poem

🐰 A rabbit hops through Linear states,
Reading, writing—revision gates!
When dimensions drift, the reconciler wakes,
Rolls back predictions, healing what it takes.
Quiet gates stand firm, no loops will break! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main fix sets in the changeset: Issues-tab status writeback (409/scope) and terminal reconciler silent-death modes.
Description check ✅ Passed The description is comprehensive and directly related to all changes in the changeset, explaining both the Issues tab fixes and terminal reconciler improvements with clear context.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/issues-writeback-revision-and-reconciler-gates

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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 and usage tips.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request enhances terminal screen convergence and remote file integration handling. Key updates include implementing a persistent dimension mismatch escalation mechanism that forces a size resync, rolling back stranded optimistic-echo predictions after a timeout, and ensuring that errors during terminal reconciliation checks do not halt the polling loop. Additionally, the IntegrationsManager is updated to support optimistic concurrency by reading the current file revision before writing (with a single retry on conflict) and allowing read-only access to the /linear/states reference subtree when a Linear integration is active. The reviewer feedback suggests using optional chaining and nullish coalescing when reading the existing file revision to prevent potential runtime errors if the response is empty.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/integrations.ts
Comment on lines +1169 to +1170
const existing = await client.readFile(handle.workspaceId, path)
baseRevision = existing.revision

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure robust defensive programming, consider using optional chaining and a nullish coalescing operator when accessing existing.revision. This prevents potential runtime TypeError crashes if the file read response is unexpectedly empty or null.

Suggested change
const existing = await client.readFile(handle.workspaceId, path)
baseRevision = existing.revision
const existing = await client.readFile(handle.workspaceId, path)
baseRevision = existing?.revision ?? '0'

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

Actionable comments posted: 3

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

Inline comments:
In `@node_modules`:
- Line 1: Remove the node_modules symbolic link from the commit and ensure
node_modules is ignored: delete the symlink entry (the tracked file named
"node_modules") from the index/commit (e.g., git rm --cached node_modules or
remove it in your staging UI) and commit that removal; then add "node_modules/"
to .gitignore (or confirm it's present) so it won't be re-added; verify this
symlink was unintentional and that dependencies are managed via
package.json/package-lock.json instead of committing node_modules.

In `@src/renderer/src/lib/terminal-reconciler.ts`:
- Around line 150-166: The code preserves dimsMismatchStreak across an early
return when plain.rows/cols differ from viewport, which lets a prior divergence
carry over and trigger a repair later; inside the mismatch branch (the block
that checks plain.rows !== viewport.rows || plain.cols !== viewport.cols) reset
dimsMismatchStreak to 0 before returning so the confirmation streak is cleared
whenever dimensions are incomparable, leaving the rest of the logic (the kick
logic using RECONCILE_DIMS_MISMATCH_CHECKS, lastDimsKickAt,
deps.onPersistentDimsMismatch, plain, viewport, now()) unchanged.

In `@src/renderer/src/lib/terminal-runtime-registry.ts`:
- Around line 379-389: The rollback timing currently uses sinceOutput =
Date.now() - lastOutputAt which incorrectly triggers rollback when lastOutputAt
is stale; change the logic to measure prediction age instead: compute the age
from a timestamp on the predictiveEcho (e.g., predictiveEcho.predictionCreatedAt
/ predictiveEcho.lastPredictionAt) such that const sincePrediction = Date.now()
- predictiveEcho.predictionCreatedAt and use sincePrediction <
STRANDED_PREDICTION_ROLLBACK_MS to gate the early return; if predictiveEcho
lacks a creation timestamp, add one when predictions are created and reference
that in the rollback check before calling predictiveEcho.rollback() and
predictiveEcho.hasPredictions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c5f24ccd-30fa-4740-8979-90cd59b9eac7

📥 Commits

Reviewing files that changed from the base of the PR and between fad4222 and 6cbb229.

📒 Files selected for processing (8)
  • AGENTS.md
  • node_modules
  • src/main/integration-remote-paths.ts
  • src/main/integrations.test.ts
  • src/main/integrations.ts
  • src/renderer/src/lib/terminal-reconciler.test.ts
  • src/renderer/src/lib/terminal-reconciler.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts

Comment thread node_modules Outdated
Comment thread src/renderer/src/lib/terminal-reconciler.ts
Comment on lines +379 to +389
const sinceOutput = Date.now() - lastOutputAt
if (predictiveEcho?.hasPredictions) {
if (sinceOutput < STRANDED_PREDICTION_ROLLBACK_MS) return false
// See STRANDED_PREDICTION_ROLLBACK_MS: the confirming echo is never
// coming; without this escape the quiet gate never reopens.
console.warn(
'[terminal] rolling back stranded optimistic-echo predictions (no confirming output)'
)
predictiveEcho.rollback()
if (predictiveEcho.hasPredictions) return 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stranded-prediction timeout is anchored to last server output, not prediction age.

At Line 379, sinceOutput is used to decide rollback timing. If lastOutputAt is old (or still 0), predictions can be rolled back on the first reconcile check instead of after a true 10s stranded period.

Suggested fix
   let activitySerial = 0
   let lastOutputAt = 0
+  let predictionsOutstandingSince = 0
   let attachSeeded = false
@@
     isQuiet: () => {
       if (disposed || !attachSeeded || currentToken === null || !opened) return false
       if (document.visibilityState !== 'visible') return false
-      const sinceOutput = Date.now() - lastOutputAt
+      const nowTs = Date.now()
+      const sinceOutput = nowTs - lastOutputAt
       if (predictiveEcho?.hasPredictions) {
-        if (sinceOutput < STRANDED_PREDICTION_ROLLBACK_MS) return false
+        if (predictionsOutstandingSince === 0) predictionsOutstandingSince = nowTs
+        if (nowTs - predictionsOutstandingSince < STRANDED_PREDICTION_ROLLBACK_MS) return false
         console.warn(
           '[terminal] rolling back stranded optimistic-echo predictions (no confirming output)'
         )
         predictiveEcho.rollback()
         if (predictiveEcho.hasPredictions) return false
       }
+      predictionsOutstandingSince = 0
       return sinceOutput >= RECONCILE_QUIET_MS
     },

Based on learnings: “Stranded predictions must roll back … after STRANDED_PREDICTION_ROLLBACK_MS of output silence.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sinceOutput = Date.now() - lastOutputAt
if (predictiveEcho?.hasPredictions) {
if (sinceOutput < STRANDED_PREDICTION_ROLLBACK_MS) return false
// See STRANDED_PREDICTION_ROLLBACK_MS: the confirming echo is never
// coming; without this escape the quiet gate never reopens.
console.warn(
'[terminal] rolling back stranded optimistic-echo predictions (no confirming output)'
)
predictiveEcho.rollback()
if (predictiveEcho.hasPredictions) return false
}
let activitySerial = 0
let lastOutputAt = 0
let predictionsOutstandingSince = 0
let attachSeeded = false
// ... other code ...
isQuiet: () => {
if (disposed || !attachSeeded || currentToken === null || !opened) return false
if (document.visibilityState !== 'visible') return false
const nowTs = Date.now()
const sinceOutput = nowTs - lastOutputAt
if (predictiveEcho?.hasPredictions) {
if (predictionsOutstandingSince === 0) predictionsOutstandingSince = nowTs
if (nowTs - predictionsOutstandingSince < STRANDED_PREDICTION_ROLLBACK_MS) return false
// See STRANDED_PREDICTION_ROLLBACK_MS: the confirming echo is never
// coming; without this escape the quiet gate never reopens.
console.warn(
'[terminal] rolling back stranded optimistic-echo predictions (no confirming output)'
)
predictiveEcho.rollback()
if (predictiveEcho.hasPredictions) return false
}
predictionsOutstandingSince = 0
return sinceOutput >= RECONCILE_QUIET_MS
}
🤖 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 `@src/renderer/src/lib/terminal-runtime-registry.ts` around lines 379 - 389,
The rollback timing currently uses sinceOutput = Date.now() - lastOutputAt which
incorrectly triggers rollback when lastOutputAt is stale; change the logic to
measure prediction age instead: compute the age from a timestamp on the
predictiveEcho (e.g., predictiveEcho.predictionCreatedAt /
predictiveEcho.lastPredictionAt) such that const sincePrediction = Date.now() -
predictiveEcho.predictionCreatedAt and use sincePrediction <
STRANDED_PREDICTION_ROLLBACK_MS to gate the early return; if predictiveEcho
lacks a creation timestamp, add one when predictions are created and reference
that in the rollback check before calling predictiveEcho.rollback() and
predictiveEcho.hasPredictions.

Source: Learnings

@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one validated issue: the PR’s read-before-write path used the writer Relayfile handle, but that handle only requested write scope. It now requests read + write scope in src/main/integrations.ts, with the integration test updated in src/main/integrations.test.ts.

Addressed comments

  • Self-review: read-before-write requires relayfile:fs:read:/** on the writer handle; fixed in src/main/integrations.ts:1633 and covered by src/main/integrations.test.ts:419.
  • Bot/reviewer comments: no separate bot or reviewer comment payload was present under .workforce, so there were no external threads to apply or reject.

Advisory Notes

  • I did not make unrelated lint-warning cleanups outside the PR diff.
  • I could not verify GitHub mergeability or cloud check status from this checkout without using git/gh.
  • I did not run the macOS-only packaged smoke job locally.

Local validation run:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

Redraw timed out once when I ran it concurrently with fidelity; rerunning it alone, matching CI order, passed.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one validated review issue in the PR scope: a dimensions-mismatch skip now resets the reconciler’s content mismatch confirmation streak, so a repair still requires fresh consecutive valid confirmations after dimensions agree again. Fix is in terminal-reconciler.ts, with regression coverage in terminal-reconciler.test.ts.

Addressed Comments

  • No bot or reviewer comment artifacts were present in .workforce; there were no external comments to validate or address.

Advisory Notes

  • None.

Local validation passed:

  • npm ci
  • npx vitest run src/renderer/src/lib/terminal-reconciler.test.ts
  • npm run verify:mcp-resources-drift && npm run lint && npm run typecheck:web && npm run typecheck:node && npm test && npx vitest run && npm run build && npm run build:web
  • npx playwright test --config playwright.fidelity.config.ts && npx playwright test --config playwright.redraw.config.ts

Lint still reports existing warnings only, with zero errors. I did not use GitHub status/mergeability checks, so I’m not marking this READY.

…iming flake

The pacer drains via setTimeout(0); on Linux CI the assertion raced it.
Matches the pattern used in the adjacent revision-dedup test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit 3467e4f into main Jun 11, 2026
4 of 5 checks passed
@khaliqgant khaliqgant deleted the fix/issues-writeback-revision-and-reconciler-gates branch June 11, 2026 14:13
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