[Bug]: An environment's threads silently stop updating until the client is restarted #4589

Description

@colonelpanic8

Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

Observed multiple times, no deterministic repro yet. The pattern:

  1. Desktop app, thread that the server already considers settled.
  2. The thread shows in the unsettled/active group anyway.
  3. Click Settle on it.
  4. Nothing happens — it stays showing as unsettled.

Expected behavior

A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

Actual behavior

The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

Restarting the desktop client clears it.

Technical detail

The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

Two candidate explanations, in order of how well they fit "nothing visibly happens":

1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

  • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
  • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
  • projection_threads.settled_override: NULL throughout
  • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

Impact

Major degradation or frequent failure

Version or commit

0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

Environment

NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

Logs or stack traces

# captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
187909 2026-07-26T20:26:21.975Z running
188015 2026-07-26T20:27:18.405Z running
188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
188071 2026-07-26T20:28:26.074Z starting
188072 2026-07-26T20:28:26.147Z running
# settle events for this streamselectsequence,event_type from orchestration_events
where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
-- (0 rows)
# receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
accepted|# projection state
projection_threads.settled_override = NULL
projection_threads.archived_at = NULL

Workaround

Restart the desktop client. Settle behaves normally afterwards.


Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Bug]: An environment's threads silently stop updating until the client is restarted #4589

      Description

      @colonelpanic8

      Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


      Before submitting

      • I searched existing issues and did not find a duplicate.
      • I included enough detail to reproduce or investigate the problem.

      Area

      apps/web

      Steps to reproduce

      Observed multiple times, no deterministic repro yet. The pattern:

      1. Desktop app, thread that the server already considers settled.
      2. The thread shows in the unsettled/active group anyway.
      3. Click Settle on it.
      4. Nothing happens — it stays showing as unsettled.

      Expected behavior

      A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

      Actual behavior

      The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

      Restarting the desktop client clears it.

      Technical detail

      The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

      Two candidate explanations, in order of how well they fit "nothing visibly happens":

      1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

      2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

      Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

      One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

      • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
      • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
      • projection_threads.settled_override: NULL throughout
      • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

      For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

      Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

      Impact

      Major degradation or frequent failure

      Version or commit

      0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

      Environment

      NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

      Logs or stack traces

      # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
      187909 2026-07-26T20:26:21.975Z running
      188015 2026-07-26T20:27:18.405Z running
      188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
      188071 2026-07-26T20:28:26.074Z starting
      188072 2026-07-26T20:28:26.147Z running
      # settle events for this streamselectsequence,event_type from orchestration_events
      where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
      -- (0 rows)
      # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
      accepted|# projection state
      projection_threads.settled_override = NULL
      projection_threads.archived_at = NULL

      Workaround

      Restart the desktop client. Settle behaves normally afterwards.


      Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Bug]: An environment's threads silently stop updating until the client is restarted #4589

          Description

          @colonelpanic8

          Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


          Before submitting

          • I searched existing issues and did not find a duplicate.
          • I included enough detail to reproduce or investigate the problem.

          Area

          apps/web

          Steps to reproduce

          Observed multiple times, no deterministic repro yet. The pattern:

          1. Desktop app, thread that the server already considers settled.
          2. The thread shows in the unsettled/active group anyway.
          3. Click Settle on it.
          4. Nothing happens — it stays showing as unsettled.

          Expected behavior

          A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

          Actual behavior

          The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

          Restarting the desktop client clears it.

          Technical detail

          The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

          Two candidate explanations, in order of how well they fit "nothing visibly happens":

          1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

          2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

          Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

          One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

          • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
          • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
          • projection_threads.settled_override: NULL throughout
          • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

          For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

          Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

          Impact

          Major degradation or frequent failure

          Version or commit

          0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

          Environment

          NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

          Logs or stack traces

          # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
          187909 2026-07-26T20:26:21.975Z running
          188015 2026-07-26T20:27:18.405Z running
          188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
          188071 2026-07-26T20:28:26.074Z starting
          188072 2026-07-26T20:28:26.147Z running
          # settle events for this streamselectsequence,event_type from orchestration_events
          where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
          -- (0 rows)
          # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
          accepted|# projection state
          projection_threads.settled_override = NULL
          projection_threads.archived_at = NULL

          Workaround

          Restart the desktop client. Settle behaves normally afterwards.


          Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Bug]: An environment's threads silently stop updating until the client is restarted #4589

              Description

              @colonelpanic8

              Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


              Before submitting

              • I searched existing issues and did not find a duplicate.
              • I included enough detail to reproduce or investigate the problem.

              Area

              apps/web

              Steps to reproduce

              Observed multiple times, no deterministic repro yet. The pattern:

              1. Desktop app, thread that the server already considers settled.
              2. The thread shows in the unsettled/active group anyway.
              3. Click Settle on it.
              4. Nothing happens — it stays showing as unsettled.

              Expected behavior

              A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

              Actual behavior

              The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

              Restarting the desktop client clears it.

              Technical detail

              The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

              Two candidate explanations, in order of how well they fit "nothing visibly happens":

              1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

              2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

              Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

              One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

              • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
              • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
              • projection_threads.settled_override: NULL throughout
              • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

              For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

              Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

              Impact

              Major degradation or frequent failure

              Version or commit

              0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

              Environment

              NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

              Logs or stack traces

              # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
              187909 2026-07-26T20:26:21.975Z running
              188015 2026-07-26T20:27:18.405Z running
              188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
              188071 2026-07-26T20:28:26.074Z starting
              188072 2026-07-26T20:28:26.147Z running
              # settle events for this streamselectsequence,event_type from orchestration_events
              where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
              -- (0 rows)
              # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
              accepted|# projection state
              projection_threads.settled_override = NULL
              projection_threads.archived_at = NULL

              Workaround

              Restart the desktop client. Settle behaves normally afterwards.


              Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Bug]: An environment's threads silently stop updating until the client is restarted #4589

                  Description

                  @colonelpanic8

                  Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


                  Before submitting

                  • I searched existing issues and did not find a duplicate.
                  • I included enough detail to reproduce or investigate the problem.

                  Area

                  apps/web

                  Steps to reproduce

                  Observed multiple times, no deterministic repro yet. The pattern:

                  1. Desktop app, thread that the server already considers settled.
                  2. The thread shows in the unsettled/active group anyway.
                  3. Click Settle on it.
                  4. Nothing happens — it stays showing as unsettled.

                  Expected behavior

                  A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

                  Actual behavior

                  The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

                  Restarting the desktop client clears it.

                  Technical detail

                  The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

                  Two candidate explanations, in order of how well they fit "nothing visibly happens":

                  1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

                  2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

                  Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

                  One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

                  • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
                  • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
                  • projection_threads.settled_override: NULL throughout
                  • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

                  For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

                  Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

                  Impact

                  Major degradation or frequent failure

                  Version or commit

                  0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

                  Environment

                  NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

                  Logs or stack traces

                  # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
                  187909 2026-07-26T20:26:21.975Z running
                  188015 2026-07-26T20:27:18.405Z running
                  188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
                  188071 2026-07-26T20:28:26.074Z starting
                  188072 2026-07-26T20:28:26.147Z running
                  # settle events for this streamselectsequence,event_type from orchestration_events
                  where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
                  -- (0 rows)
                  # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
                  accepted|# projection state
                  projection_threads.settled_override = NULL
                  projection_threads.archived_at = NULL

                  Workaround

                  Restart the desktop client. Settle behaves normally afterwards.


                  Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Bug]: An environment's threads silently stop updating until the client is restarted #4589

                      Description

                      @colonelpanic8

                      Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


                      Before submitting

                      • I searched existing issues and did not find a duplicate.
                      • I included enough detail to reproduce or investigate the problem.

                      Area

                      apps/web

                      Steps to reproduce

                      Observed multiple times, no deterministic repro yet. The pattern:

                      1. Desktop app, thread that the server already considers settled.
                      2. The thread shows in the unsettled/active group anyway.
                      3. Click Settle on it.
                      4. Nothing happens — it stays showing as unsettled.

                      Expected behavior

                      A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

                      Actual behavior

                      The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

                      Restarting the desktop client clears it.

                      Technical detail

                      The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

                      Two candidate explanations, in order of how well they fit "nothing visibly happens":

                      1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

                      2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

                      Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

                      One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

                      • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
                      • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
                      • projection_threads.settled_override: NULL throughout
                      • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

                      For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

                      Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

                      Impact

                      Major degradation or frequent failure

                      Version or commit

                      0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

                      Environment

                      NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

                      Logs or stack traces

                      # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
                      187909 2026-07-26T20:26:21.975Z running
                      188015 2026-07-26T20:27:18.405Z running
                      188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
                      188071 2026-07-26T20:28:26.074Z starting
                      188072 2026-07-26T20:28:26.147Z running
                      # settle events for this streamselectsequence,event_type from orchestration_events
                      where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
                      -- (0 rows)
                      # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
                      accepted|# projection state
                      projection_threads.settled_override = NULL
                      projection_threads.archived_at = NULL

                      Workaround

                      Restart the desktop client. Settle behaves normally afterwards.


                      Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Bug]: An environment's threads silently stop updating until the client is restarted #4589

                          Description

                          @colonelpanic8

                          Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


                          Before submitting

                          • I searched existing issues and did not find a duplicate.
                          • I included enough detail to reproduce or investigate the problem.

                          Area

                          apps/web

                          Steps to reproduce

                          Observed multiple times, no deterministic repro yet. The pattern:

                          1. Desktop app, thread that the server already considers settled.
                          2. The thread shows in the unsettled/active group anyway.
                          3. Click Settle on it.
                          4. Nothing happens — it stays showing as unsettled.

                          Expected behavior

                          A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

                          Actual behavior

                          The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

                          Restarting the desktop client clears it.

                          Technical detail

                          The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

                          Two candidate explanations, in order of how well they fit "nothing visibly happens":

                          1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

                          2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

                          Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

                          One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

                          • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
                          • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
                          • projection_threads.settled_override: NULL throughout
                          • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

                          For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

                          Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

                          Impact

                          Major degradation or frequent failure

                          Version or commit

                          0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

                          Environment

                          NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

                          Logs or stack traces

                          # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
                          187909 2026-07-26T20:26:21.975Z running
                          188015 2026-07-26T20:27:18.405Z running
                          188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
                          188071 2026-07-26T20:28:26.074Z starting
                          188072 2026-07-26T20:28:26.147Z running
                          # settle events for this streamselectsequence,event_type from orchestration_events
                          where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
                          -- (0 rows)
                          # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
                          accepted|# projection state
                          projection_threads.settled_override = NULL
                          projection_threads.archived_at = NULL

                          Workaround

                          Restart the desktop client. Settle behaves normally afterwards.


                          Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Bug]: An environment's threads silently stop updating until the client is restarted #4589

                              Description

                              @colonelpanic8

                              Update — root cause identified. This is not specific to settling. A client's subscriptions to an environment can die permanently after a transport blip while the connection stays healthy, leaving that environment's threads frozen until the client restarts. "Settle does nothing" was just the first reachable symptom. See the confirmed server-side evidence, the mechanism in subscribeDynamic, and the dead-subscription vs dead-connection asymmetry. The original report follows unchanged.


                              Before submitting

                              • I searched existing issues and did not find a duplicate.
                              • I included enough detail to reproduce or investigate the problem.

                              Area

                              apps/web

                              Steps to reproduce

                              Observed multiple times, no deterministic repro yet. The pattern:

                              1. Desktop app, thread that the server already considers settled.
                              2. The thread shows in the unsettled/active group anyway.
                              3. Click Settle on it.
                              4. Nothing happens — it stays showing as unsettled.

                              Expected behavior

                              A settled thread displays as settled. Failing that, clicking Settle converges the view — or says why it can't.

                              Actual behavior

                              The thread displays as unsettled while the server considers it settled, and clicking Settle does nothing at all — no state change, no toast, no error, no feedback of any kind. There is no way to move the thread into the settled group from the UI. Repeated clicks are equally silent.

                              Restarting the desktop client clears it.

                              Technical detail

                              The command round-trip is itself a state sync: threadUpsertOrRemove (apps/server/src/ws.ts:725-750) re-reads the current projected thread row and emits a thread-upserted carrying it after any thread event — including the idempotent settle re-emit below. So a settle sent against a client that disagrees with the server should return the authoritative row and converge the client.

                              Two candidate explanations, in order of how well they fit "nothing visibly happens":

                              1. The settle succeeds and the client never applies the resulting update.thread.settle on an already-settled thread re-emits thread.settled with the originalsettledAt and the existingupdatedAt (apps/server/src/orchestration/decider.ts:489-505), deliberately, so bulk-settle and double-click stay quiet no-ops. Success is not toasted (it is a high-frequency lifecycle action). So if the client's shell stream is stalled or its snapshot is stale, every click succeeds server-side, emits an authoritative thread-upserted, and the user sees absolutely nothing change — indefinitely. A client restart reloads the HTTP shell snapshot and resubscribes, which is consistent with restart being the only known fix.

                              2. A client-side pre-send block.settleThread (apps/web/src/hooks/useThreadActions.ts:415) returns without sending when readEnvironmentSupportsSettlement(...) is false (line 419 — fires if the environment's server config is missing or stale, since threadSettlement defaults to false on decode) or when canSettle(...) is false (line 433 — fires while the client's copy of session.status is starting | running, packages/client-runtime/src/state/threadSettled.ts:86). Note this path is not silent in the web app: every settle call site routes through SidebarV2.attemptSettle (:1690), which surfaces a "Failed to settle thread" toast on failure. So this explains a settle that visibly fails, not one that does nothing.

                              Worth noting: canSettle is used in exactly two places, both action handlers, and never to disable or hide the control — so the control is always live regardless of state.

                              One instance captured in detail, from state.sqlite, stream 9cf5c431-…. Caveat: this instance shows the server having never settled the thread, so it is likely path 2 rather than the already-settled case, and may be a distinct bug presenting similarly. Including it for the forensics:

                              • orchestration_command_receipts: nothread.settle receipt for the aggregate, and no rejected rows — the command never reached the server
                              • orchestration_events: nothread.settled / thread.unsettled rows for the stream, ever
                              • projection_threads.settled_override: NULL throughout
                              • thread.session-setready at 20:27:44.228Z (turn completed), next starting not until 20:28:26.074Z

                              For what it's worth, the server-side sync design looks sound on inspection — computeSnapshotSequence takes the min across required projectors inside the same transaction as the projection reads (ProjectionSnapshotQuery.ts:160-180), and a missing or too-large afterSequence falls back to a full snapshot (ws.ts:1286-1327). If this is a stale-view bug, the interesting question is what leaves a client subscribed but not applying updates.

                              Suggested mitigation regardless of root cause: have the settle action reconcile the thread's authoritative state when it is pressed, rather than acting on a local view that may be stale, and give the user feedback when a settle cannot change anything. Today a no-op success and a healthy settle are indistinguishable from the UI.

                              Impact

                              Major degradation or frequent failure

                              Version or commit

                              0.0.28-flake desktop build from a local integration stack on top of upstream main @ 5719e8ac4

                              Environment

                              NixOS, Electron 41.9.1 desktop app, Node 24.18.0, claudeAgent provider (Opus)

                              Logs or stack traces

                              # captured instance — session status transitions (orchestration_events, stream 9cf5c431-…)
                              187909 2026-07-26T20:26:21.975Z running
                              188015 2026-07-26T20:27:18.405Z running
                              188051 2026-07-26T20:27:44.228Z ready <-- settle attempted in the window after this
                              188071 2026-07-26T20:28:26.074Z starting
                              188072 2026-07-26T20:28:26.147Z running
                              # settle events for this streamselectsequence,event_type from orchestration_events
                              where stream_id='9cf5c431-…' and event_type in ('thread.settled','thread.unsettled');
                              -- (0 rows)
                              # receipts for this aggregate: no thread.settle, nothing rejectedselectstatus,count(*) from orchestration_command_receipts where aggregate_id='9cf5c431-…' group by status;
                              accepted|# projection state
                              projection_threads.settled_override = NULL
                              projection_threads.archived_at = NULL

                              Workaround

                              Restart the desktop client. Settle behaves normally afterwards.


                              Possibly related but distinct: #4561 and #4584 land in the same user-visible family (thread won't settle / stuck showing Working), but both are server-side persisted-state bugs where projection_thread_sessions itself is wrong after a stop-then-quit or a mid-turn app death. Here the disagreement is between the server's view and the client's copy of it.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions