fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, '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

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) - #70

Merged
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor
Apr 24, 2026
Merged

fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)#70
tyulyukov merged 3 commits into
mainfrom
marcode/port-shell-authoritative-store-refactor

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Summary

Correctness fix. Removes the client-derived buildSidebarThreadSummary path that was re-deriving server-authoritative sidebar flags (hasPendingApprovals, hasPendingUserInput, hasActionableProposedPlan, latestUserMessageAt) from detail state during every writeThreadState / legacy read-model sync — overwriting the shell stream's server-computed values in the window before the next shell event arrives.

This is exactly the bug MEMORY.md warned against ("ghost 'Pending Approval' badges on resolved threads"), which upstream #1996 fixes by partitioning write ownership between the shell stream and the detail stream.

Why this was missed

My UPSTREAM_DIVERGENCE.md and PR #69 both claimed the store.ts changes in upstream pingdotgg#1996 were "cleanup with no behavioral delta." Wrong. The describe block store.test.ts:1345 "shell events are authoritative for sidebar summary flags" already encodes the right invariant, but the two tests inside it were relying on applyOrchestrationEvent("thread.activity-appended", …) populating sidebarThreadSummaryById via buildSidebarThreadSummary — i.e., they were asserting the buggy intermediate state.

Changes

apps/web/src/store.ts

  • Delete buildSidebarThreadSummary() (lines 319-340 pre-fix) and getLatestUserMessageAt() (its only caller).
  • Remove unused imports from ./session-logic: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan. (Their own unit tests in session-logic.test.ts still exercise them — keep the functions, stop importing them here.)
  • writeThreadState (detail-stream writer): drop nextSummary computation + sidebarThreadSummariesEqual-guarded write block. Add doc comment documenting stream ownership.
  • buildThreadState (legacy read-model full rebuild): drop sidebarThreadSummaryById from the return-type Pick and stop populating it. The shell stream is the sole writer; syncServerReadModel (MarCode-exclusive fallback) preserves any existing sidebar summary via its ...state spread.
  • writeThreadShellState: add doc comment mirroring upstream's stream-ownership guidance.

apps/web/src/store.test.ts

The two tests inside describe("shell events are authoritative for sidebar summary flags") are rewritten to match the corrected contract:

  • Detail-stream events MUST NOT write sidebarThreadSummaryById. After thread.activity-appended / thread.proposed-plan-upserted, selectSidebarThreadSummaryByRef returns undefined — the detail stream did nothing.
  • Shell-stream events populate the summary from the server-computed flag.makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }) sets the flag on; { hasPendingUserInput: false } clears it.
  • Extended both tests to cover the transition (true → false) to retain the original "shell wins authoritatively" assertion.

What I deliberately did NOT port from upstream pingdotgg#1996

  • Structural reorganization of store.ts (the initialEnvironmentState commentary block, the ensureThreadRegistered helper extraction into a named fn, the retainThreadScopedRecord helper changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win.

Test plan

  • bun run typecheck — clean monorepo-wide
  • apps/web test suite — 1095 / 1095 passing (including the rewritten regression-guard describe block)
  • oxlint — 0 errors (67 pre-existing warnings)
  • Manual smoke:
    • Thread with pending approval → sidebar badge shows → resolve the approval → badge clears immediately on the next shell event
    • Thread with pending user input → sidebar badge shows → answer the input → badge clears
    • No ghost "Pending Approval" badges on completed threads after any long-running session
  • FEATURES.md checklist — no exclusive feature regresses (Incremental Event Handling is exactly the feature this PR tightens)

…(upstream pingdotgg#1996)
Removes the client-derived buildSidebarThreadSummary path that was
overwriting server-authoritative sidebar flags during the window between
a detail-stream write and the next shell event. Matches the stream-
separation contract established in upstream pingdotgg#1996 and required by
MEMORY.md ("ghost Pending Approval badges on resolved threads").
Changes in apps/web/src/store.ts:
- Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only
used by the broken path).
- Remove session-logic imports: derivePendingApprovals,
derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan
(their tests in session-logic.test.ts still exercise them).
- writeThreadState (detail stream): stop computing nextSummary; drop the
sidebarThreadSummariesEqual block. Detail stream no longer touches
sidebarThreadSummaryById.
- buildThreadState (legacy read-model path): drop sidebarThreadSummaryById
from the return type Pick; stop populating it. The shell stream is the
sole writer.
- Add doc comments to writeThreadState + writeThreadShellState explaining
stream ownership, mirroring upstream pingdotgg#1996's guidance.
Changes in apps/web/src/store.test.ts:
- Update the "shell events are authoritative for sidebar summary flags"
describe block's two tests to reflect the new contract: detail-stream
events do NOT populate sidebarThreadSummaryById; shell events are the
sole writer. Extended both tests to also verify the later-shell-event
transition (true → false) still wins authoritatively.
What we deliberately did NOT port from upstream pingdotgg#1996:
- Structural reorganization of store.ts (initialEnvironmentState
commentary, ensureThreadRegistered helper extraction). MarCode's
Incremental Event Handling & Structural Sharing (FEATURES.md §1)
already covers this functionally via syncServerReadModel. Dragging
in upstream's cosmetic reshuffle risks regression for no behavioral
win.
- Split upstream pingdotgg#1996 into two ledger entries for the two MarCode PRs
that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93a)
and PR #70 (store.ts shell-stream-authority correctness refactor,
506b808).
- Remove "NOT yet equivalent" note in the Already-equivalent section now
that both halves are ported.
- Drop pingdotgg#1996 from Pending real work; only pingdotgg#2246 remains.
- Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md)
to the pingdotgg#2246 row so the next cycle's porter has the staged-commit strategy.
Corrects the earlier claim that pingdotgg#1996's store.ts refactor was "cleanup
with no behavioral delta" — it's a real correctness fix aligning MarCode
with the stream-separation contract MEMORY.md already requires.
@tyulyukov
tyulyukov merged commit 3a16dfe into mainApr 24, 2026
5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov