Repository navigation
Start and stop one isolated application without restarting the shared workers - #3339
Conversation
… workers A new isolated application's first deploy with restart: true now starts only its own dedicated worker, held as the canary that certifies the release. A drop of an isolated application stops only its worker, with or without restart, and removes its branched databases unless keepBranches is set. Dropping a component that is not on a node restarts nothing there, and a replicated drop reports each node's componentRemoved, workerStopped, branchesRemoved and error. Main owns a whole-component drop (a worker hands it over and waits), and adds the shared pool to a named restart only when a pool worker reported loading that application. Fixes #3045, fixes #3091. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
…the restart's turn Read isolation for a drop from the root config file instead of a refresh that exits the process on a malformed file; evaluate pool ownership when a restart takes its turn and count every live pool worker; withdraw a refused isolated slot, surviving worker included, before the decision publishes; start no new dedicated worker when refusals cannot be read, and hold an in-memory refusal only while its release is live; keep the error class across the worker hand-off; decide branch retention before an early answer; let main add the pool to an isolated deploy instead of a disk heuristic; size the dedicated slot's ready bound from the canary window; separate the drop's start deadline from its wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
…lot's withdrawal Refuse keepBranches together with restart: true, since a peer on an earlier version ignores the field and its restart removes the branches. Withdraw a refused isolated application's slot by force, queued before the decision is read and awaited by the rollout's close, so a worker the withdrawal stops can still read the decision of the deploy it requested. Carry the error class and code across the worker hand-off, read peer outcomes flat or wrapped, stop an isolated worker before waiting for peers, and keep a scoped reconcile's heap share to the workers it actually runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
…'s admission Answer an isolated application that drops itself only after the peers have answered, so replication errors and peer results reach it. Keep a reconcile of one application from counting or reporting other applications that are not running, preserve the error name across the worker hand-off for classes other than ClientError and ServerError, and log when an incomplete restart keeps an application's branched databases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
Errors render by constructor name, so a drop error other than ClientError or ServerError that a worker rebuilds from main's answer is rebuilt as a class carrying the original name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
A reconcile of one application now admits in the same order boot does, so a restart keeps the same applications running where capacity is short; it still reports refusals only for the application it reconciles. A deferred load no longer records an application whose isolated value is invalid as placed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
The record only ever widens a restart, and a value fixed before the deferred load runs would otherwise leave a worker that loaded the application unreported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
There was a problem hiding this comment.
Code Review
This pull request refactors the component drop and restart lifecycle, ensuring that starting, stopping, or dropping an isolated application does not affect unrelated workers. It centralizes whole-component drops on the main thread under a component lock, introduces a keepBranches option, and implements targeted worker reconciliation. Feedback on these changes suggests enhancing robustness by safely handling nullish or primitive errors in catch blocks and IPC message handling, as well as wrapping secondary package.json updates in a try-catch block to prevent minor failures from disrupting the core drop operation.
…t fork to go On Bun a replacement starts only once its predecessor has exited, so the thread list shows only new workers while one of them has not yet reported what it loaded; until it has, the drop correctly keeps the fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045
dawsontoth
left a comment
There was a problem hiding this comment.
Yusss! I can run local tests of this tomorrow if you like, or merge and abuse it later, whatever works. Concept looks great!
A new isolated app deployed from a worker thread never gets its worker, so every replicated peer misses itFound while testing locally at d37b2a1 (macOS, Node 24.18). What happens. A first
Root cause (from temporary log lines in With nothing wanted, no canary starts, and Possible fix. The drop path already handles this case: Test gap. Every deploy in The rest checked out locally:
🤖 Generated with Claude Code |
Brings in #3339 (start and stop one isolated application), which this branch's PR lists as overlapping. Its two new DESIGN.md index entries left the root index at 251 lines, over its 250 budget (main's own lint fails on it), and this branch's notes on top took components/DESIGN.md to 1035 of 1000. Related index entries now share a bullet, this branch's deployment-record notes are tighter, and the dangling-symlink note drops its history, so both budgets hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… results For HarperFast/harper#3339 (v5.4.0): a new isolated application's first restart deploy starts only its own worker, as its canary; drop_component stops an isolated application's worker with or without restart, removes its forks by default (keepBranches keeps them), restarts nothing on a node that does not have the application, and reports each node's outcome. Adds the one-call preview cleanup for clusters on v5.4.0, and a 5.4 release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com>
… results For HarperFast/harper#3339 (v5.4.0): a new isolated application's first restart deploy starts only its own worker, as its canary; drop_component stops an isolated application's worker with or without restart, removes its forks by default (keepBranches keeps them), restarts nothing on a node that does not have the application, and reports each node's outcome. Adds the one-call preview cleanup for clusters on v5.4.0, and a 5.4 release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com>
… drop results (#750) * Document scoped start/stop of isolated applications and per-node drop results For HarperFast/harper#3339 (v5.4.0): a new isolated application's first restart deploy starts only its own worker, as its canary; drop_component stops an isolated application's worker with or without restart, removes its forks by default (keepBranches keeps them), restarts nothing on a node that does not have the application, and reports each node's outcome. Adds the one-call preview cleanup for clusters on v5.4.0, and a 5.4 release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: scope claims to whole-project drops and restart=true, check every node's outcome Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: a self-drop answers before its worker stops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: the Operations API drop waits for its restart; name when the shared workers restart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: qualify the pool restart when isolation turns on; cite the issue that set log-only removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: forks a drop could not remove are retried automatically (harper#3600) Since harper#3600 (v5.4.0-alpha.2), a drop that does not keep its forks records the removal as owed and Harper retries it until it succeeds, instead of only logging a failure or waiting for a second drop. Also lists the main-thread-serves case among those that keep forks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_documentation_750-2919ad88 Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: check workerStopped in the one-call cleanup, date the preview restart allowance, qualify certification for linked directories and single-file drops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_documentation_750-2919ad88 Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: GPT-5 Codex <noreply@openai.com> Co-authored-by: Gemini <noreply@google.com>
… drop results (HarperFast/documentation#750) * Document scoped start/stop of isolated applications and per-node drop results For #3339 (v5.4.0): a new isolated application's first restart deploy starts only its own worker, as its canary; drop_component stops an isolated application's worker with or without restart, removes its forks by default (keepBranches keeps them), restarts nothing on a node that does not have the application, and reports each node's outcome. Adds the one-call preview cleanup for clusters on v5.4.0, and a 5.4 release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: scope claims to whole-project drops and restart=true, check every node's outcome Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: a self-drop answers before its worker stops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: the Operations API drop waits for its restart; name when the shared workers restart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: qualify the pool restart when isolation turns on; cite the issue that set log-only removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA Dispatch-Task: harper-3045 Dispatch-Task: documentation-750-rebase Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: forks a drop could not remove are retried automatically (harper#3600) Since harper#3600 (v5.4.0-alpha.2), a drop that does not keep its forks records the removal as owed and Harper retries it until it succeeds, instead of only logging a failure or waiting for a second drop. Also lists the main-thread-serves case among those that keep forks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_documentation_750-2919ad88 Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> * Address review: check workerStopped in the one-call cleanup, date the preview restart allowance, qualify certification for linked directories and single-file drops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_documentation_750-2919ad88 Dispatch-Task: pr-maint-da77693db6a28c0dbcc9d24f2a6a2e45 Co-Authored-By: Gemini <noreply@google.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: GPT-5 Codex <noreply@openai.com> Co-authored-by: Gemini <noreply@google.com>
⊙ Problem
Every new PR-preview deploy (
isolated: true,restart: true) restarts every shared worker on every node, and its first release is not canary-certified. Cleanup is worse:drop_componentleaves the preview's worker running unlessrestart: true, restarts the shared workers when the app is not on a node, and reports nothing per node.🤔 Should #3045 and #3091 ship as one PR?
Yes: both change the same drop path, and one drop to the cluster URL helps only once it both stops the worker and reports per node. Docs: Document starting and stopping one isolated application, and per-node drop results (documentation#750).
💡 Solution
restart: truedeploy starts only its own worker. That worker is the canary: a release that fails to load leaves no worker and no socket.restart, and removes its branched databases unlesskeepBranches: true.drop_componentanswers{ node, componentRemoved, workerStopped, branchesRemoved };replicatedcarries the same per peer, pluserror.⚖️ Alternatives
restartWorkers— that reloads every root component on main; main never loads isolated apps.Also weighed
errorshows it.🤔 Should dropping an absent app still discard branches an earlier drop kept?
Yes, chosen: otherwise
keepBranches, or a shared drop without restart, leaves storage no later drop can remove. Nothing restarts, and while a shared worker may still hold the fork it stays.🤔 Should
keepBranchesbe refused together withrestart: true?Chosen: an older peer ignores
keepBranches, and its restart path removes the forks; without a restart it keeps them. A shared app with forks then drops and restarts in two calls; easy to relax later.🤔 Is the node's own result at top level, with peers in
replicated, the right shape?Chosen to match
deploy_component, where a peer returns the same object. The alternative: onenodesarray including the origin.🔧 Changes
components/operations.js— deploy names an isolated app as the restart scope; the certification accepts a new isolated app. A whole-component drop runs on main and is answered by main for a worker; the worker hands it over and waits within its budget, and main refuses to start it late. Main reads isolation from the file rather than a refresh that exits on a bad config, removes the app, decides retention before any early answer with a reason in the message, and keeps the error class. File drops keep their path, plus the absent no-op.server/threads/manageThreads.js—leavesNothingToLoad, queues its slot's withdrawal before the decision publishes, and stays registered until it drains. Pool ownership comes from each worker's report viapoolMayHold, read when the restart takes its turn, adding the pool to a named restart, which reconciles only that app. The request and answer helpers match replies by id.server/threads/socketRouter.ts— admission reports only for the reconciled app; failed-closed apps get no worker, and a forced withdrawal ignores a failed read; heap share counts real slots; the slot's ready bound covers a canary, and a withdrawn slot is not a failure.components/componentLoader.ts,server/threads/heldStart.ts— each worker records what it placed, deferred loads included, and reports it before admission.components/canaryRollout.ts— eligibility receiveswasAbsent.components/operationsValidation.js—keepBranches: a strict boolean;keepBranches: trueis refused withrestart: true.bin/restart.ts— the worker-drop branch-removal path main now owns is removed.utility/hdbTerms.ts— two ITC message types.✅ Verification
isolated-application-lifecycle.test.ts: 14 pass. Thread ids and mirrors across deploy, refusal (also across a restart), drops from main, a pool worker and the app itself, absent drops, both flips, the shared-drop-then-isolated sequence, and an unrelated pending app.branched-database-package.test.ts(+3 per placement, and a certified first isolated deploy): fork removed by default, kept withkeepBranches, then discarded by a second drop. 12 pass.isolated-application.test.ts(7),canary-rollout.test.ts(10) and 9 new unit tests pass: the gate and main requests (with a fixture hook), eligibility, and peer results and validation. Every new test that checks new behavior fails on base.test:unit:main(7313),test:unit:resources(4407) andtest:integration:all(2561; 6 Ollama cases cancel locally) pass.🤖 Generated with Claude Code
https://claude.ai/code/session_015gDmwTaExpc8uXcdBNr8eA
🤖 Generated by Anthropic Claude (Opus 5.5, dispatch dev-agent); posted via @kriszyp.
Release target: main — new behavior and a new drop_component field, no linked milestone
Related PRs: 3 independent
Complexity: complicated
Deferred findings
f7a59ff0061ac9c2nit reliability —components/operations.js— branch-removal failure after an early answer only logged — NOT FILEDOrigin — the dispatch brief this PR was written from
Implement #3045 (start and stop one isolated application without restarting the shared HTTP workers) together with #3091 (per-node results from a replicated drop_component; dropping an absent component is a no-op). One PR closing both. Part of epic #3042 (PR preview deployments): today every new PR preview's first deploy rolling-restarts every shared worker on every node, and cleanup has to call each node directly and verify it by hand.
Decided behavior:
Acceptance
host,urlPath,isolatedandbranchedDatabases#3043) or deploy permissions (Scope deploy permissions to component names #3046).Dispatch: task
harper-3045· queued by kris-session · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse,muse; rounds=8; full=2 @ d37b2a1
Review-Attention: deep ~25m (hot: heldStart.ts, manageThreads.js +1; decisions: do-less-alternative, test-retries-until-reported, config-order-reservation, error-identity-by-name, self-drop-waits-for-peers, keepbranches-refused-with-restart, withdrawal-fences-redeploy, default-branch-deletion, absent-drop-succeeds) @ d37b2a1