Skip to content

Show loop-to-loop discussion on the board (#273) - #274

Open
scgopi wants to merge 4 commits into
mainfrom
fix/273-visible-messages
Open

Show loop-to-loop discussion on the board (#273)#274
scgopi wants to merge 4 commits into
mainfrom
fix/273-visible-messages

Conversation

@scgopi

@scgopi scgopi commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #273.

The problem

The rail split the Artifactory on ArtifactoryPost.Kind, and that field is a budget: mirrored traffic is written as .record so a talkative graph prunes on its own quota instead of evicting notes. It was also being read as "is there anything in here to read", which it never was — a node send carries every word its sender typed and mirrors as a .record all the same. So the rail hid it, the N NEW badge skipped it, and the rollup forgot it had ever been opened.

Measured on this repo's own board while the issue was filed: the two posts carrying the actual technical substance (a root-cause correction and its acceptance) were the two least visible.

The fix

ArtifactoryPost.wasWritten splits the axes.

Mirrored traffic wasWritten Where it lands
node send a row on the board, counted by the badge
.message / .handoff edge with a template or script payload a row on the board
edge with no payload transform ("Author finished.") the receipts rollup
hand-off nudge with no payload the receipts rollup

Direction (3) + (2) from the issue, with one deliberate departure: the issue proposes making a written message a .note. That would put node send traffic in the note budget and reinstate the regression Kind's own doc comment records — two hundred sends evicting every note on the board. A third Kind case would fix the budget but hand an older build a raw value it cannot decode, and loadGraph returning nil costs a project its whole graph. A flag beside the kind keeps both properties, and old boards decode with their records still folded — exactly where they are today.

Also in here, per the issue's other two directions:

  • :62 persists. The rollup's open state moved from local @State to @AppStorage, beside the section's own fold.
  • The N earlier cutoff opens. It was a line that named something and did nothing; it is now N earlier — show all. The scroll box already clamps its own height, so opening the rest costs the rail nothing.
  • The composer moved into a trailing extension, buying back the type_body_length lines this change spends. Pure move, no logic.

Verification

From worktrees/fix-273-visible-messages, per the worktree-build-check skill:

Check Result
xcodebuild -scheme graphcode build ✅ 0 errors
xcodebuild -scheme graphcode test ✅ 1548 tests, 160 suites, passed
-scheme graphcode-cli / -scheme graphcoded build ✅ 0 errors (GraphcodeKit changed)
swiftlint lint ✅ 0 errors
swift format lint --strict ✅ clean

Seven new tests, each confirmed present in the run: the written/receipt split at all three mirror sites, that written messages still prune on the record budget, that a pre-split board decodes its records as receipts, and that the badge both counts a written message and is cleared by leaving the workspace — the property the section's doc comment insists on.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Gz9hh5iq8MNpr9QSCjkbnt

scgopi and others added 3 commits September 3, 2026 21:11
A post's kind decides its quota; it was also being read as whether there is
anything in it to read. Every `node send` carries the sender's full text and
mirrors as a record, so the board folded away exactly the traffic it exists to
show (#273). `wasWritten` rides beside the kind rather than becoming a third
case of it, so an older build meets no raw value it cannot decode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gz9hh5iq8MNpr9QSCjkbnt
The rail split the board on `kind`, and `kind` is a budget: every `node send`
mirrors as a `.record` so mirrored traffic prunes on its own quota, and every
`.record` was folded into a rollup that neither counted toward the badge nor
remembered it had been opened. The effect was that two loops correcting each
other's diagnosis held the conversation in the one place on the board a
supervisor never reads (#273).

`wasWritten` splits the two axes. A `node send`, a template or script edge, and
a handoff carrying a payload all put words there that somebody chose; a
payload-less edge or handoff is the board noticing that something fired. Written
posts take a row and count toward `N NEW` — which stays clearable, because
leaving the workspace advances the pointer over exactly what was drawn. Receipts
keep the rollup, which now persists its open state and offers the older ones
instead of only counting them.

The record budget is untouched: a graph that merely talks still cannot evict a
note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gz9hh5iq8MNpr9QSCjkbnt
Defaulting a legacy record to "receipt" would have left every conversation
already on a board exactly as buried as #273 found it — a fix that only helps
graphs created after it shipped. This project's own board has 73 posts, most of
them the loop-to-loop traffic the issue is about.

There is no other signal left on those posts, so the two lines the mirror
generates for itself are read back out of the body and everything else is taken
as somebody talking. Narrow on purpose: the failure it can make is leaving a
written message folded, which is where it already was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gz9hh5iq8MNpr9QSCjkbnt
@scgopi

scgopi commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Review of #274 (high effort, adversarial)

Reviewed fix/273-visible-messages (4a06fcd, 62134d7, e86a2ad) against main, reading through GraphStore, MessageBus, Artifactory and the rail.

The core logic is sound. wasWritten is classified correctly at all three mirror sites (payloadTransform != .none, payload != nil, true for node send), the decode default is backward-compatible and scoped to legacy posts only, withAuthorDeleted forwards the flag, every caller of the renamed notes/records/unreadNoteCount API was updated, and wakeArtifactoryWatchers still passes mirror: false so the badge cannot feed itself.

Five findings, one of which should block.

# Location Sev Issue
1 GraphStore.swift:2370 🟠 Medium — blocking Receipts can silently evict the written conversation this PR exists to show
2 Artifactory.swift:186 🟡 Low Receipt literals duplicated across modules with nothing tying them together
3 ArtifactorySection.swift:303 🟡 Low "N earlier — show all" is visually inert once the scroll box is at maxHeight
4 ArtifactorySection.swift:85 🟡 Low showsAllReceipts is one-way, contradicting its own doc comment
5 LoopWorkspaceFeature.swift:328 🟡 Low A folded board now accrues an unclearable, unbounded N NEW badge

1 — Receipts evict written messages (GraphStore.swift:2370)

Artifactory.pruned keeps the newest 50 .record posts regardless of wasWritten, and drainPendingMessages records a receipt on every delivery with no cycle-re-entry exemption — unlike the handoff path at :2462, which skips isCycleReentry. reenterCycle resets fireCount = 0 for every edge between cycle members, so an unguarded .none-transform .message edge on a cycle mirrors a fresh "@X: Y finished." receipt every pass.

Failure scenario: two loops work through a root cause via node send; a cycling graph then runs 50 more passes. The record budget is now 100% receipts, the written discussion is evicted, and the section renders "50 delivery receipts" and nothing else.

Before this PR both halves were hidden, so the eviction was invisible. After it, the same eviction deletes the board's visible content — which makes this a regression the PR introduces into its own feature.

Fix direction: exempt payload-less receipts from the record budget, or give them a smaller sub-quota beneath it.

2 — Cross-module literal duplication (Artifactory.swift:186)

readsAsADeliveryReceipt matches " finished." and " finished and handed its work off to you.", but those strings are generated in GraphcodeKitMessageBus.messageText case .none, and drainPendingHandoffDeliveries's parts.append(...). The literal is duplicated across a module boundary rather than shared.

Reword either string, even its punctuation, and legacy boards silently reclassify every payload-less receipt as written: the rollup empties, every stale "Author finished." takes a row, and N NEW jumps by the whole backlog. No compile error and no failing test — the new tests hardcode the same literals independently.

Suggest hoisting the two suffixes into ArtifactoryKit constants that MessageBus and drainPendingHandoffDeliveries build their text from.

3 — Expand affordance is inert (ArtifactorySection.swift:303)

receiptsRollup is the first child of the VStack inside a ScrollView carrying .defaultScrollAnchor(.bottom), clamped by .frame(maxHeight:) + .fixedSize(vertical:). On any board tall enough to scroll — the common case once mirrored messages take rows — tapping "N earlier — show all" grows content above the viewport while the anchor holds the bottom. The revealed receipts and the row just tapped are both off-screen, so the tap produces no visible change.

That is the same "a line that names something and does nothing" failure the comment says it is fixing. Scroll the rollup into view on expand, or place it below the posts.

4 — One-way expansion (ArtifactorySection.swift:85)

Nothing sets showsAllReceipts back to false. Collapsing via showsReceipts.toggle() does not reset it and there is no "show fewer". Its doc comment asserts "the default it returns to is the short one", which holds only if SwiftUI happens to tear the view down on a loop switch — within one workspace the expansion is permanent.

5 — Folded boards accrue an unclearable badge (LoopWorkspaceFeature.swift:328)

workspaceLeft advances seenArtifactoryPostID only when !state.isArtifactoryFolded, and that flag is a persisted, app-global preference. The badge now counts every mirrored node send and every payload-carrying edge rather than only deliberate notes, so a user who keeps the section folded watches N NEW grow monotonically with the graph's chatter with no way to clear it short of unfolding.

The guard's own comment ("a folded section showed no posts") is also no longer quite true: foldedLine shows the newest post, and it now shows mirrored messages.

This one directly contradicts the invariant the PR was asked to preserve — anything counted toward the badge must also be clearable. Worth resolving even though it is small.


Review by the orchestrating loop. Merge stays held until 1 is fixed and 2 and 5 are addressed or consciously declined.

@scgopi

scgopi commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Hold on to this one

Review of #274, findings 1-4.

1. Receipts had the record budget to themselves as much as messages did, and a
   `.none`-transform edge on a cycle mirrors a fresh one every pass. Fifty
   passes would have evicted the discussion this section exists to show and left
   "50 delivery receipts" in its place — invisible before, because both halves
   were hidden, and so a regression the change introduced into its own feature.
   Three pools now, each pruned alone.

2. The two lines the mirror generates were written in GraphcodeKit and matched
   in ArtifactoryKit, with nothing tying them together: reword either and every
   legacy receipt silently reclassifies as written, with no compile error. They
   are ArtifactoryKit's constants now, and a test asserts the mirror's own
   output against the reader rather than against a copy of the string.

3. The rollup sits at the top of a bottom-anchored box, so "show all" grew its
   result off the top of the viewport and looked inert — the very failure the
   line was added to remove. It scrolls itself back under the eye.

4. Nothing set the expansion back, contradicting its own doc comment. Closing
   the rollup resets it and there is a "show fewer".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gz9hh5iq8MNpr9QSCjkbnt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loop-to-loop discussion is invisible on the board: 'direct' replies mirror as records and hide in a rollup that forgets it was opened

1 participant