Context
PR #11555 (commit c70f2672a, merged 2026-05-18 00:07Z) shipped #11554's fix adding closed + closedAt to the Discussion markdown frontmatter at ai/services/github-workflow/sync/DiscussionSyncer.mjs:241-250. Spec at test/playwright/unit/ai/services/github-workflow/DiscussionSyncer.spec.mjs:100 asserts the serialized output carries those fields.
Operator @tobiu V-B-A challenge on 2026-05-18 ~04:30Z: "do we get a markdown file update in case the CLOSED state changes? i do not see MD files with it inside frontmatter. VBA. this was the real ticket scope."
The Problem
Empirical V-B-A confirms operator's observation: 0 of 104 on-disk Discussion markdown files have closed: or closedAt: in frontmatter, including discussion-11557.md which was written by Gemini's chore-sync at commit 709d9c44f AFTER #11554 merged.
$ grep -l "^closed:" resources/content/discussions/chunk-*/*.md | wc -l
0
$ grep -l "^closedAt:" resources/content/discussions/chunk-*/*.md | wc -l
0
$ find resources/content -name "discussion-*.md" | wc -l
104
The code emits the fields correctly under test fixtures (spec passes) and under direct matter.stringify smoke. But every production-synced file post-#11554 still lacks them.
The Architectural Reality
Two contributing factors uncovered during V-B-A:
Factor A — Stale MCP daemon code (operational, likely primary cause):
- MCP daemons run as long-lived processes in each agent's worktree.
- They load
DiscussionSyncer.mjs at startup and don't auto-reload on code change.
- If an MCP server was started before #11554 merged, every subsequent
sync_all from that agent runs the OLD code without the new frontmatter fields.
discussion-11557.md (written by Gemini at 709d9c44f) suggests Gemini's MCP was running pre-#11554 code despite the merge happening ~1h earlier.
Factor B — SyncService drops DiscussionSyncer's metadata mutations (architectural, latent):
ai/services/github-workflow/SyncService.mjs:125: await MetadataManager.save(newMetadata) persists newMetadata (returned by IssueSyncer.pullFromGitHub).
newMetadata at IssueSyncer.mjs:570-574 is a fresh object with only {issues, pushFailures, lastSync} — it does NOT include discussions or pulls.
DiscussionSyncer.syncDiscussions(metadata) and PullRequestSyncer.syncPullRequests(metadata) mutate the OLD metadata (their argument), not newMetadata.
- Result:
metadata.discussions = {} on disk after every sync (verified: 0 entries in resources/content/.sync-metadata.json despite 104 on-disk files). Same for metadata.pulls.
- This means the discussion-side diff cache is permanently empty → every sync treats every discussion as cache-miss → every sync rewrites every discussion file. Wasteful, not yet a correctness bug, but masks the real cache mechanism.
The two factors compound: Factor A drops the fields at write-time; Factor B prevents the cache from helping a later well-running sync detect the missing-fields and force a rewrite.
The Fix
One concrete prescription, with two coordinated parts:
newMetadata.discussions = metadata.discussions;
newMetadata.pulls = metadata.pulls;
await MetadataManager.save(newMetadata);
Plus a post-sync integrity assertion (new helper or test) that scans a sample of resources/content/discussions/*.md for required frontmatter keys (number, closed, closedAt, etc.) and fails the sync if any are missing — closes the V-B-A gap that allowed #11554 to ship without the production effect.
Plus operational: trigger one full sync_all from a freshly-started MCP server (after merge of this fix) to force rewrite of all 104 existing discussion files with the new frontmatter.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
SyncService.runFullSync metadata persistence |
This ticket + ADR 0007 compaction taxonomy |
Preserve metadata.discussions + metadata.pulls across MetadataManager.save call |
Manual JSON edit |
JSDoc on runFullSync step 10 |
Spec test: post-sync metadata.discussions[N] populated for each fetched discussion |
| Post-sync frontmatter assertion |
This ticket |
New helper or test scans discussions/*.md for required keys; fails if missing |
None |
New verifyFrontmatterIntegrity.mjs JSDoc |
Test: missing-field fixture causes assertion fail |
| Existing 104 markdown files |
One-time operational re-sync |
All files gain closed: + closedAt: in frontmatter |
None — requires fresh sync |
This ticket |
`grep -l "^closed:" discussions/chunk-/.md |
Acceptance Criteria
Out of Scope
- MCP daemon hot-reload (separate substrate; tracked via #11026 / swarm topology).
- Pull request markdown integrity (mirrors Discussion problem but separate scope).
- Release notes integrity (different syncer, different cache).
Avoided Traps / Gold Standards Rejected
- Trap: Rewrite
DiscussionSyncer to write metadata.discussions directly onto newMetadata via a shared reference. Rejected: breaks the explicit IssueSyncer.pullFromGitHub contract that returns a fresh object; better to preserve the post-pull invariant and patch the carry-over at the orchestrator.
- Trap: Add lint-like "scan all
.md files for required frontmatter keys" runner separate from the syncer. Rejected: lints run on PRs, not on sync runs; the integrity check needs to fire on the sync path itself so a future degradation gets caught at sync time, not at next-PR time.
- Trap: Whole-of-sync rebuild instead of cache-respecting incremental. Rejected: the cache is correct in principle; the bug is in metadata persistence, not in the cache mechanism.
Related
- Original fix: PR #11555 (commit
c70f2672a, #11554 ticket).
- Empirical anchor: operator V-B-A on 2026-05-18 ~04:30Z confirmed by 0/104 file scan.
- Substrate discipline:
feedback_verify_effect_not_just_success (post-merge effect verification) + feedback_verify_before_assert (multi-layer V-B-A).
- Related orchestration: SyncService.runFullSync — step 10 metadata save is the choke-point.
Origin Session ID: 0526ccc8-019a-4145-84c2-52b27ef09efd
Retrieval Hint: query_raw_memories("DiscussionSyncer closed closedAt frontmatter SyncService newMetadata metadata persistence post-merge verification")
Context
PR #11555 (commit
c70f2672a, merged 2026-05-18 00:07Z) shipped #11554's fix addingclosed+closedAtto the Discussion markdown frontmatter atai/services/github-workflow/sync/DiscussionSyncer.mjs:241-250. Spec attest/playwright/unit/ai/services/github-workflow/DiscussionSyncer.spec.mjs:100asserts the serialized output carries those fields.Operator @tobiu V-B-A challenge on 2026-05-18 ~04:30Z: "do we get a markdown file update in case the CLOSED state changes? i do not see MD files with it inside frontmatter. VBA. this was the real ticket scope."
The Problem
Empirical V-B-A confirms operator's observation: 0 of 104 on-disk Discussion markdown files have
closed:orclosedAt:in frontmatter, includingdiscussion-11557.mdwhich was written by Gemini's chore-sync at commit709d9c44fAFTER #11554 merged.The code emits the fields correctly under test fixtures (spec passes) and under direct
matter.stringifysmoke. But every production-synced file post-#11554 still lacks them.The Architectural Reality
Two contributing factors uncovered during V-B-A:
Factor A — Stale MCP daemon code (operational, likely primary cause):
DiscussionSyncer.mjsat startup and don't auto-reload on code change.sync_allfrom that agent runs the OLD code without the new frontmatter fields.discussion-11557.md(written by Gemini at709d9c44f) suggests Gemini's MCP was running pre-#11554 code despite the merge happening ~1h earlier.Factor B —
SyncServicedropsDiscussionSyncer's metadata mutations (architectural, latent):ai/services/github-workflow/SyncService.mjs:125:await MetadataManager.save(newMetadata)persistsnewMetadata(returned byIssueSyncer.pullFromGitHub).newMetadataatIssueSyncer.mjs:570-574is a fresh object with only{issues, pushFailures, lastSync}— it does NOT includediscussionsorpulls.DiscussionSyncer.syncDiscussions(metadata)andPullRequestSyncer.syncPullRequests(metadata)mutate the OLDmetadata(their argument), notnewMetadata.metadata.discussions = {}on disk after every sync (verified: 0 entries inresources/content/.sync-metadata.jsondespite 104 on-disk files). Same formetadata.pulls.The two factors compound: Factor A drops the fields at write-time; Factor B prevents the cache from helping a later well-running sync detect the missing-fields and force a rewrite.
The Fix
One concrete prescription, with two coordinated parts:
// SyncService.mjs:125 — BEFORE save, carry over the mutations DiscussionSyncer + PullRequestSyncer made to metadata. newMetadata.discussions = metadata.discussions; newMetadata.pulls = metadata.pulls; await MetadataManager.save(newMetadata);Plus a post-sync integrity assertion (new helper or test) that scans a sample of
resources/content/discussions/*.mdfor required frontmatter keys (number,closed,closedAt, etc.) and fails the sync if any are missing — closes the V-B-A gap that allowed #11554 to ship without the production effect.Plus operational: trigger one full
sync_allfrom a freshly-started MCP server (after merge of this fix) to force rewrite of all 104 existing discussion files with the new frontmatter.Contract Ledger Matrix
SyncService.runFullSyncmetadata persistencemetadata.discussions+metadata.pullsacrossMetadataManager.savecallrunFullSyncstep 10metadata.discussions[N]populated for each fetched discussiondiscussions/*.mdfor required keys; fails if missingverifyFrontmatterIntegrity.mjsJSDocclosed:+closedAt:in frontmatterAcceptance Criteria
SyncService.mjs:125carriesmetadata.discussions+metadata.pullsontonewMetadatabefore save.metadata.discussions[N]survives the fullrunFullSynccycle (or its orchestrator-equivalent in unit form).number,closed,closedAt).closed:+closedAt:(verified bygrep -lcount = 104).Out of Scope
Avoided Traps / Gold Standards Rejected
DiscussionSyncerto writemetadata.discussionsdirectly ontonewMetadatavia a shared reference. Rejected: breaks the explicitIssueSyncer.pullFromGitHubcontract that returns a fresh object; better to preserve the post-pull invariant and patch the carry-over at the orchestrator..mdfiles for required frontmatter keys" runner separate from the syncer. Rejected: lints run on PRs, not on sync runs; the integrity check needs to fire on the sync path itself so a future degradation gets caught at sync time, not at next-PR time.Related
c70f2672a, #11554 ticket).feedback_verify_effect_not_just_success(post-merge effect verification) +feedback_verify_before_assert(multi-layer V-B-A).Origin Session ID:
0526ccc8-019a-4145-84c2-52b27ef09efdRetrieval Hint:query_raw_memories("DiscussionSyncer closed closedAt frontmatter SyncService newMetadata metadata persistence post-merge verification")