Skip to content

Companion's systems, first slice: signals and one page loader - #112

Merged
Pelotrio merged 12 commits into
1.21.1from
claude/systems-slice
Sep 30, 2026
Merged

Pelotrio merged 12 commits into
1.21.1from
claude/systems-slice

Conversation

@Pelotrio

@Pelotrio Pelotrio commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

First PR of docs/SYSTEMS.md: the few systems every Companion feature uses to learn of changes, load pages, watch files, run work, write and receive game messages, and the rules that keep features on them. It brings the first mechanisms with their first users, to show the design holds for a page that only shows and for one that edits before anything else moves.

Finish line

Each behavior is proven with a test that pauses a read, or follows the user's path, rather than only the parts:

Behavior Tests
Opening and navigation cause one necessary read PageReadsTest (Key bindings), ResourceTabReadsTest (a mod's resource): opened through navigation in the application's window (offscreen), one read; navigating to the shown page, none
Hidden tabs defer follow-up reads, retries included the same two tests (a change while hidden, one read when shown, none when shown again without one); PackResourceEditorReadsTest (the editor's first read paused, the tab left, the file saved elsewhere: no read while hidden, one when shown); PageLoaderTest.aPageHiddenWhileItReadsReadsNothingMoreUntilShownWhoeverAsks, manyChangesWhileHiddenAreAskedAboutOncePerSource
Saving and refreshing keep edits and conflict checks ResourceTextEditorTest and TextureEditorTest, including aSaveAfterTheWorkingPackMovedBeforeTheTabHeardGoesIntoNoPack; PageLoaderTest.aReadUnderWayWhenThePageHoldsIsNotShownAndIsReadAgain; CatalogPanelsTest.keyBindingsKeepTheirSelectionWhenTheirKeysAreReadAgain
Closing a page keeps late results from it PageLoaderTest.aFailureIsReportedWithItsCauseAndNothingIsShownAfterDisposing

What changes

  • Signal: an owner's change, carrying nothing; the follower asks the owner. The catalog, change record, packs (per side), resource edits and key assignments fire one instead of keeping their own listener lists. ResourceEdits fires only when something was written.
  • PageLoader: a page names itself (page) and the signals it follows (follows). It reads when first shown, and after a signal once it is shown; no read runs for a hidden page, whoever asks; missed changes are kept once per source. A follow can say whether a change concerns the page, and a read can ask which signals led to it (fired). A page that writes holds its reads (hold/release). A failed read is tried again when the page is shown again. The old modes stay for pages not moved yet.
  • Key bindings page (only shows): no read in the constructor, on navigation or after its own change. It shows the keys KeyAssignments holds, which reads options.txt again right after Companion's own write, and always reads its first value, also where the game's folder cannot be watched yet. Selection and scroll are kept through Tables.keepingSelection.
  • Resource editor (edits): its read and write counters, follow flags and three subscriptions become the loader's page, follows, fired and hold. Hidden resource tabs no longer read on every reload. A save checks the pack it goes into when it is made.
  • SystemsRulesTest parses Companion's sources and lists every file that makes its own thread, listener list, file watcher or shared-pool async call today, with why. A new one fails the build, and a listed one that goes away must leave the list.
  • AGENTS.md points every agent to docs/SYSTEMS.md.

Production code: +620 −408. Mechanisms arrive here; the later PRs mostly delete.

Known, left to PR 4

KeyAssignments is replaced by the shared file reading in PR 4, which updates state on one serialized path. Two theoretical cases from the last review round are left to it rather than patched here:

  • the Key bindings page's first read overlapping the owner's own first read within the same moment, where a key saved right then is not told;
  • a system with no file watching at all, where keys rebound in the game are not found.

Also known and left as it is: two resource saves that overlap exactly, one refused, can tell the Files tab twice, which then reads its pack list once more than needed. Nothing shown is wrong.

:companion:test passes (1614 tests).

🤖 Generated with Claude Code

Pelotrio and others added 2 commits September 30, 2026 11:21
docs/SYSTEMS.md describes the few systems every feature of Companion uses, and the rules that keep features on
them. This is its first PR: the mechanisms with their first users.

- Signal: an owner's change, carrying nothing. The catalog, the change record, the packs, the resource edits and
  the key assignments fire one instead of keeping their own listener lists.
- PageLoader: a page names itself and the signals it follows. It reads when first shown, and after a signal once
  it is shown; a follow can say whether a change concerns the page; a page that writes holds its reads.
- The Key bindings page, which only shows, and the resource editor, which edits, move completely onto it. They
  no longer read in their constructors or on navigation, and hidden resource tabs no longer read on every reload.
  The editor's read and write counters and follow flags become the loader's hold.
- Key assignments are read again right after Companion's own write, so the page shows it without reading again.
- Tables.keepingSelection keeps a table's selection and scroll when it is read again.
- SystemsRulesTest lists every file that makes its own thread, listener list, watcher or shared-pool call
  today; a new one fails the build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…les test counts more

From an adversarial review of the slice:

- KeyAssignments always reads its first value, also where the game's folder cannot be watched yet, and keeps it
  when a newer write overtakes that read, so no later change goes untold. The Key bindings page shows the keys it
  holds rather than reading options.txt itself; unwatched, it reads them whenever the page is shown.
- A page whose read failed reads again when it is shown again.
- The resource editor releases its held reads in finally, so nothing thrown can leave it holding them.
- SystemsRulesTest also counts then...Async calls without an executor, commonPool, startVirtualThread,
  java.util.Timer, statically imported executor factories, Thread subclasses, and callbacks kept in any
  collection; the files that do so today are listed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T11:36:47.573533Z 15a68ae New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Pelotrio and others added 4 commits September 30, 2026 11:49
… pack the read found

The pack a mod's resource is saved into is known only once the copies are read. A change of its entry in the
change record while the tab opened, or moved to another working pack, was taken as not concerning the tab and
dropped, leaving the content read before on screen. After each read the entry is compared again, as before the
slice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…another pack

The slice made every read of the copies hold Save back, also one after a failed save or an edit elsewhere, so a
Save pressed during it did nothing; CI's slower machine hit that in aSaveOverTextAnotherTabSavedSinceAsksFirst. As
before the slice, only the first read and one after a pack or working pack change hold it. The page loader tells a
read's preparation which followed signals led to it (fired), rather than signals carrying what changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ir target when made

From the review's finish line for the slice, each behavior now proven with a read paused while something else
happens:

- A page named with page() reads nothing while hidden, also when the page itself asks, as the resource editor does
  to check a finished read against a later change; it reads once when shown. Changes missed while hidden are kept
  once per source, however often they came.
- The resource editor's save checks the pack it goes into when it is made: a working pack or world that changed
  before the tab heard of it is refused, and the tab reads the pack it saves into now, keeping the edit unsaved.
- ResourceEdits tells its edits only when a save, revert or adoption wrote something.
- ResourceTabReadsTest opens a mod's resource through navigation and counts its reads, as PageReadsTest does
  for the Key bindings page; PackResourceEditorReadsTest pauses the editor's first read while its tab is left and
  the file is saved elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A texture's animation lands before its image; when the image then fails, what landed stays and is recorded, so
the resource listing must read it. Only a save refused before it wrote, as over a copy changed since, or one that
found nothing to write, tells nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c150e7484c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +152 to +155
Throwable cause = failure;
while (cause instanceof CompletionException && cause.getCause() != null) cause = cause.getCause();
boolean wrote = failure == null ? saved != null : !(cause instanceof ChangePipeline.Stale);
if (wrote) this.edited.fire();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fire edited only after a write lands

When a save fails before reaching pipeline.write—for example, because the selected folder pack was deleted or the project write executor rejects the task—the unwrapped cause is an IOException, so this treats the operation as having written and fires edited. PackResourcesPanel follows that signal and re-joins every pack, an operation its own comment identifies as too slow to repeat, even though no state changed. Track whether any write actually landed rather than treating every non-Stale failure as a write.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Pelotrio and others added 5 commits September 30, 2026 12:37
… edit ended

The last two rounds guessed from the outcome: a failure told nothing, then every failure but a refusal told. Both
were wrong somewhere, a texture's animation landing before its image failed, or a save failing before it wrote.
ResourceEdits now counts each file it writes into a pack or the options, and each program's save it takes, and an
edit tells when that count moved while it ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A folder removed while watched, or missing when the project opened, left the keys read before on screen: its
watch had ended, yet the page took the keys as watched. KeyAssignments now tries the watch again after 1 and 5
seconds and every 30 seconds after, reads the keys fresh whenever asked meanwhile, and once watching reads and
tells what changed. A read on request is not what the next change is told against, so every follower hears it. The
page's own read-when-shown fallback goes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The helper waited before the read that showing starts had begun, a Swing step later; on CI's slower machine that
read was still running when aHeldPageReadsWhatItMissedOnceReleased held the page, which then rightly read once
more after release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ange is told against

A page that read the keys before the owner's first read, followed by the game saving others, left the owner taking
the new keys as its first and telling nobody; the page kept the old ones. The first read now sets what later reads
compare with, whoever makes it; reads on request after it still leave that to the owner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Where the game's folder can be read but not watched, the retries only tried the watch again, so a key rebound in
the game was never found. Each retry now also reads the file and tells a change, at most every 30 seconds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a436119540

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +153 to +156
private CompletableFuture<Saved> finished(Supplier<CompletableFuture<Saved>> edit) {
long before = this.landed.get();
return edit.get().whenComplete((saved, failure) -> {
if (this.landed.get() != before) this.edited.fire();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track landed files per resource operation

When two saves or reverts overlap, both can snapshot the same process-wide landed value; after one operation writes and waits for its reload, the other can be refused without writing yet still observe the first operation's increment and fire edited(). The successful operation later fires again, causing PackResourcesPanel to perform its costly pack join twice even though only one edit landed. The fresh evidence versus the earlier comment is the newly added global AtomicLong, which still cannot associate a landing with its originating operation; track landed writes per operation instead.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

The tests asserted a read count right after a change, which CI's slower machine sometimes had not reached yet: a
read under way was still being shown, so the next one started a moment later. They now wait for the count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Pelotrio
Pelotrio merged commit 990d778 into 1.21.1 Sep 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant