Skip to content

Systems 6: the Project tree's folders and external edits register with the one watcher - #117

Merged
Pelotrio merged 4 commits into
claude/systems-ownersfrom
claude/systems-readings
Oct 1, 2026
Merged

Pelotrio merged 4 commits into
claude/systems-ownersfrom
claude/systems-readings

Conversation

@Pelotrio

@Pelotrio Pelotrio commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Sixth PR of docs/SYSTEMS.md, stacked on #116: the last two watchers of their own, FileUtils' and ExternalEdits', become registrations with the one FileWatch.

Finish line

Behavior Test
Companion moves a watched folder and one inside it while their watches are paused FileWatchTest.aWatchedFolderAndOneInsideItCanBeMovedWhileTheWatchIsPaused
A follower added during a pause, also a nested one, watches nothing there until the pause ends FileWatchTest.aFollowerAddedWhileAPauseLastsWatchesNothingThereUntilItEnds
An operation that fails still watches again and tells what it changed FileWatchTest.aFailedOperationWatchesAgainAndTellsWhatItChanged
Other folders are told while a pause lasts FileWatchTest.otherFoldersAreToldWhileAPauseLasts
A folder reached through a link is paused and watched again FileWatchTest.aFolderReachedThroughALinkIsPausedAndWatchedAgain
A Project tree folder is told of entries created and removed, not of writes FileWatchTest.aListingIsToldOfEntriesCreatedAndRemovedButNotOfWrites
A folder deleted and made again is told and watched again FileWatchTest.aListingOfAFolderDeletedAndMadeAgainIsToldAndWatchedAgain
A closed project takes no save from an image editor and holds no folder of the pack ExternalEditsTest.aClosedProjectTakesNoSaveAndHoldsNoFolderOfThePack
A texture is taken once it is whole ExternalEditsTest.aFollowedTextureIsTakenOnceItIsWhole
No watcher, thread or executor of their own is left SystemsRulesTest (the exceptions of FileUtils and ExternalEdits are gone)

The pause, entries-only and closing tests each fail without their change. The nested-pause test also holds the outer pause past the retry delay and proves no early callback. The junction test runs only on Windows.

What changes

  • FileWatch gains watchEntries for a folder's listing, told only of entries created or removed, and pausing(roots, operation). The pause lets go of every watch on or inside the roots, runs the operation, watches them again and tells their followers, also where the operation failed. On Windows a folder cannot be moved while a folder inside it is watched.
  • The Project tree's folders and the decompiled sources follow their folders with watchEntries; moving script folders runs in FileWatch.pausing.
  • FileUtils, which was only its own watcher thread, pause and registrations, goes, and with it DirectoryWatcherTest. Its tests of its two-lock registration have no counterpart, since FileWatch registers under one lock; its other behaviors are in FileWatchTest.
  • ExternalEdits registers each followed file with FileWatch; its settle runs on the timer and takes saves on a strand, and closing lets go of every watch. Each take carries the write it settled, so a later write supersedes a task already handed to the strand. Opening in another program runs on the file workers. The results of its saves stay a listener per file, an event like script runs.
  • docs/SYSTEMS.md records the pause and entries-only listings, and a decision: the Logs page, the configuration pages and the resource packs listing keep reading whenever they are shown (readsWhenShown) instead of getting readings. They are their files' only readers, and the game writes its logs the whole time it runs, so a reading would only add a watch. The step that planned those readings goes; the pages PR moves the three pages.

Left as it is

A watch placed while its folder is paused waits for the pause to end or its retry after a second, whichever comes first.

A Project tree folder's watch is let go on the Swing thread under FileWatch's lock, which a native registration also holds; a registration that stalls, as on a hung network drive, would hold the Swing thread that long. The game folder is local.

Focused :companion:test checks pass locally. CI runs the full build for the updated head.

🤖 Generated with Claude Code

…ileWatch

FileWatch gains watchEntries, told only of entries created or removed, and
pausing, which lets go of every watch on or inside some folders while Companion
moves them. FileUtils, which was only its own watcher, goes; ExternalEdits
registers each file with FileWatch and settles on the timer and a strand.
SYSTEMS.md records the pause and that the logs, configuration and resource packs
pages keep reading whenever they are shown.

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-30T17:19:19.512548Z 30cee53 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.

@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: b24d03c06d

ℹ️ 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 +125 to +126
if (file.pending != null) file.pending.cancel(false);
file.pending = this.settle.schedule(() -> take(file), SETTLE_MILLIS, TimeUnit.MILLISECONDS);
file.pending = Workers.later(SETTLE_MILLIS, this.strand, () -> take(file));

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 Discard superseded settle tasks before reading

When the 300 ms timer has already handed this task to strand, pending.cancel(false) cannot remove it from the strand queue. If another write event arrives before that queued task runs—for example while the shared owner pool is busy—the stale task reads the newer write immediately instead of allowing it to settle for 300 ms. For non-PNG resources, readable accepts partial content, so an editor writing in stages can cause an intermediate file to be recorded and reloaded before the replacement task runs. Carry a generation into take, as FileReading does, so superseded handoffs exit before reading.

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

Useful? React with 👍 / 👎.

void aFolderReachedThroughALinkIsPausedAndWatchedAgain() throws Exception {
Path real = Files.createDirectory(this.directory.resolve("real"));
Path alias = this.directory.resolve("alias");
Process link = new ProcessBuilder("cmd", "/c", "mklink", "/J", alias.toString(), real.toString()).redirectErrorStream(true).start();

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 Guard the Windows-only junction setup

On Linux and macOS, this test fails immediately because cmd is unavailable, so running :companion:test locally cannot complete. Other junction-dependent tests in this repository, such as ScriptFilesTest, use @EnabledOnOs(OS.WINDOWS); apply the same guard here or create a platform-appropriate symbolic link.

Useful? React with 👍 / 👎.

@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: 330db57a86

ℹ️ 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 +132 to +136
for (Watched followed : this.watched) {
if (followed.key != null) continue;
if (followed.retry != null) followed.retry.cancel(false);
place(followed, 0);
tell.add(followed.changed);

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 Restore only followers whose outermost pause ended

When pausing calls are nested, the inner finally processes every follower with a null key, including followers still covered by the outer pause. It invokes their changed callbacks before the outer operation has finished, so a FileReading or ExternalEdits registration can read/adopt intermediate state if the remaining operation lasts beyond its settle delay. Track which followers this pause released, and restore or notify them only after their last covering pause ends.

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

Useful? React with 👍 / 👎.

@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: e5d2534b65

ℹ️ 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 +133 to +135
synchronized (this) {
// Another write came since, whose own settle takes the file.
if (this.closed || write != file.writes) return;

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 Revalidate the take after reading the file

If a later write event or project close occurs after this check but while Files.readAllBytes is in progress, as can happen with a large or slow resource, the task still calls adopt with content its generation no longer owns; non-PNG partial bytes can consequently be recorded and reloaded, and an in-flight take can survive close(). The fresh race remains after the queued-task fix because validation occurs only before the blocking read; recheck closed and write == file.writes after reading, as FileReading.readHere does before publishing.

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

Useful? React with 👍 / 👎.

Comment on lines +135 to +140
for (Watched followed : this.watched) {
if (followed.key != null) continue;
if (followed.pausedAt != null && paused(followed.pausedAt)) continue;
if (followed.retry != null) followed.retry.cancel(false);
place(followed, 0);
if (followed.pausedAt == null) tell.add(followed.changed);

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 Restore only followers affected by this pause

When any unrelated follower currently has no key because its folder is unwatchable, its pausedAt is also null, so every script-file pausing operation enters this branch, cancels that follower's scheduled retry, retries it immediately, and invokes its callback even though its folder is outside roots. The fresh case left after the nested-pause fix is specifically a null key caused by an unavailable watcher or unwatchable/network folder rather than by another pause; retain the set of followers actually released by this operation and restore only those.

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

Useful? React with 👍 / 👎.


this.stopWatching = watch
? FileUtils.startNewDirectoryWatcher(path, () -> {
? FileWatch.shared().watchEntries(path, () -> {

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 Preserve recovery for linked tree directories

When a displayed directory is a symlink or Windows junction and its target is deleted and later recreated, this migration leaves the tree stale. After the target's watch key becomes invalid, FileWatch.place tests the logical link path, finds it is no longer a directory, and watches the link's parent; recreating the target changes only the target's parent, so no event reattaches the watch or clears the tree's error state. The former FileUtils registration resolved and retried the real target path, while createChildIfPresent deliberately admits linked directories, so FileWatch needs to preserve recovery through the real target before this consumer moves to it.

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

Useful? React with 👍 / 👎.

@Pelotrio
Pelotrio added this pull request to stack #121 October 1, 2026 11:43
@Pelotrio
Pelotrio merged commit 113f374 into 1.21.1 Oct 1, 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