Repository navigation
MP-H: two-client multiplayer harness (tools/mp.mjs) - #4
Open
aurablacklight wants to merge 11 commits into
Open
aurablacklight wants to merge 11 commits into
aurablacklight wants to merge 11 commits into
Conversation
The packet worker could not run the live checks in its sandbox. Add what the coordinator observed when running them outside it, including what failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UTWx6KTPBs4koTLg8uZMHX
…r review - A client whose DevBridge cannot bind its port is detected from its log, and relaunched once after the TIME_WAIT window, bounded by --timeout. - Server and client telemetry is read from logs/debug.log, so debug-level agentcraft.mp events are visible to harness tests. - Gradle preparation is bounded by --timeout, the launch export ensures client assets, and the process identity no longer depends on the caller's time zone or locale. - Fall back to the checkout's .gradle-home and check for Java 25, like the other launchers. Document and pin the --json summary shape. - Windows: portable test fixture paths, -ExecutionPolicy Bypass, and the wrapper started through Start-Bg. None of this was run on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnFLfHG1rVtWmS9c6mAQP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnFLfHG1rVtWmS9c6mAQP
aurablacklight
marked this pull request as ready for review
October 4, 2026 05:22
main gained the CI workflow (.github/workflows/ci.yml). Nothing else changes on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnFLfHG1rVtWmS9c6mAQP
This was referenced Oct 4, 2026
down and status no longer read the launch environment (heap sizes, the Gradle command), so a stale or invalid value cannot block cleanup. The log cursor carries a generation, a digest of the log's first lines, next to its offset, so a log that a later up replaced is read from its start even after it has grown past the old offset. The launch-metadata build carries its run marker on the Gradle command line, so down can find and stop it when the launcher died before the PID was saved. Each has a test that failed before the fix. 64 tools tests pass. Not run: the harness itself, and nothing on Windows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
| const startsWithExe = executable => executable && (p.command.startsWith(`${executable} `) || p.command.startsWith(`"${executable}" `)); | ||
| // gradlew execs Java, so a build has no stable executable prefix: require its init script too. | ||
| const direct = startsWithExe(entry.executable) || (entry.kind === 'foreman' && p.command.trim() === entry.marker) || | ||
| (entry.kind === 'build' && p.command.includes(exportScript(root))); |
| const until = Math.min(deadline, now() + timeoutMs); | ||
| while (now() < until && members.some(member => sameProcess(member, root, stamp))) await delay(Math.min(100, Math.max(0, deadline - now()))); | ||
| for (const member of members.filter(member => sameProcess(member, root, stamp))) { | ||
| if (platform === 'win32') shell(root, `Stop-OwnTree ${member.pid} ${psQuote(member.startTime)} | Out-Null`); |
| })); | ||
| scan(); | ||
| if (now() >= deadline) break; | ||
| if (replies.every(reply => reply.status === 'fulfilled' && reply.value)) return states; |
| } else { | ||
| // Direct POSIX launches own a detached group. Signal its tools too, while the leader's | ||
| // PID/start identity still matches, then retain member identities across leader exit. | ||
| try { kill(record.groupPid === record.pid ? -record.pid : record.pid, 'SIGTERM'); } |
…tops (MP-H) withLock: two controllers that both judged a lock stale could both take it, because the stale lock was deleted and recreated. A takeover now claims the next generation with an exclusive mkdir and stands down unless its own owner record is the live one. Windows: the dedicated server was killed with no wait, so the reused world was never saved. up now seeds a loopback RCON port with a random password for the run, down sends stop through it and waits for the exit before any kill. Whenever a server is killed, down says that it had no final save. A .bat or .cmd Gradle override on Windows runs through PowerShell (Node refuses to spawn one without a shell). Ports are checked again right before the processes start. The game clients are launched with AGENTCRAFT_DEV_REMOTE=1, the opt-in the remote dev commands now need; the server and the Foremen are not. From the pull-request review. 69 tools tests pass. Nothing here was run on Windows or against a real server: the RCON stop rests on the 26.3 sources and a test listener. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
… (MP-H) On POSIX the start time from ps has whole seconds, so a PID reused within that second passed the identity check and could be signalled. Recovery and stop now also require that the process's command line still carries the entry's launch marker (a recorded child: that it is still in its leader's process group), read right before each signal. Windows is unchanged: its start times have 100 ns. From the pull-request review. 70 tools tests pass, and real child processes were recovered and stopped on Linux. Not run on macOS: down there depends on ps showing the marker for every harness process, which the live macOS run should confirm before this merges. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
From the second automated review of the pull request, each claim checked against the code first: - An unfinished build is recovered only from a process shaped like the Gradle launch itself (the default launcher, or Java running Gradle with the harness's arguments). A command that merely quotes that command line, such as a grep for it, carried the marker and the init script path too and could be signalled. - On Windows, stopping an unfinished build kills its process tree except a Gradle daemon the build started (opt-in switch on Stop-OwnTree; other callers unchanged). Not yet run on Windows. - waitForClients checks for an interruption or a lost process again after the dev.state requests, before it accepts readiness. - On POSIX a client that acknowledged dev.quit gets up to five seconds to exit by itself before SIGTERM; one that did not answer is signalled at once. Four new tests, each failing before its fix; tools tests 74 of 74. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
… (MP-H) Two up/down cycles with two clients on Windows: the server saved over RCON and exited by itself, nothing was left running, the shared Gradle daemon stayed, and the second cycle reused the world and its plots. The worknote lists what the run reached and what is still unit-covered only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLkzPTC3pm16WCmLm9dqyp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Wave 0 packet MP-H of the multiplayer campaign (
docs/multiplayer/work-packets.md).What this adds
node tools/mp.mjs up | status | down --slot <n>: one dedicated server, one or two dev clients and their Foremen, on the campaign's port block for slotn, each client with its own game directory, profile and user name.tools/mp.mjs,tools/lib/mp/{config,launch,processes,harness,index}.mjs: slot and port math, capped direct JVM launches (2 GB server, 2 GB per client by default), pid plus start-time identity for every process, durable launch intents, readiness waits, rollback on a failedup, idempotentdown, and a small library for tests (wait for all clients, DevBridge commands on client A or B, server and client telemetry reads).tools/lib/mp/export-launch.gradle: an init script that exports Loom's real run classpaths and arguments without running either run task or editingbuild.gradle.tools/test/mp-*.test.mjs: 51 tests.tools/README.md: usage, resource budget, recovery, the--jsonsummary shape.The local server binds to loopback only, offline mode, no whitelist, no RCON. Nothing outside
tools/and the worknote is touched.Verification
npm test --prefix tools: 61 pass, 0 fail, 0 skipped (macOS, Node 24).upstarted immediately after the previousdownon the same slot. Everyupsucceeded, every client reported ready over DevBridge, and no process was left after the lastdown. Harness JVMs: about 2.0 GB resident with one client, about 2.6 GB with two.Review
Two reviewers, and a separate checker for the six findings they rated major: 17 findings in all, 15 fixed in the commits after the first. The worknote's "Review fixes" section says what each one led to.
upsoon after adownused to wait out--timeout, because the new client's DevBridge cannot bind its port while the old connections are in TCP TIME_WAIT. The harness now reads the client'sDevBridge server errorfrom its log, waits 35 s and relaunches that client once, inside--timeout. Both restarts in the live run went through this path.logs/debug.loginstead of the stdout capture, which Loom's dev logging limits to INFO, so the contract's debug-level events are visible. There is areadClientEventstwin.-ExecutionPolicy Bypass, the wrapper started throughStart-Bg). Graceful server shutdown on Windows still needs stdin or a remote command; a forced stop can lose changes since the last autosave.mod/DEV.mddocuments whyDevBridgedoes not set address reuse (on Windows it would let two games bind the same port).setReuseAddr(!isWindows)would remove the 35 s wait on macOS and Linux, if you want it.🤖 Generated with Claude Code
https://claude.ai/code/session_017nnFLfHG1rVtWmS9c6mAQP