Run PortOS's App Management update through the detached launcher - #5985
Merged
Conversation
Owner
Author
|
Required code review was not completed before publication. This PR is intentionally left open and will not be merged until the required review completes. |
Owner
Author
|
Review status for this run (configured reviewers:
Per this run's required-review rule, the missing codex verdict blocks the merge. |
…#5976) Updating PortOS from App Management stopped portos-server and never brought it back, even when the pull was a no-op. PortOS is itself a managed app, so that button reaches update.sh through appUpdater's plain spawn rather than routes/update.js. update.sh's own `pm2 delete ecosystem.config.cjs` then tree-kills portos-server — and PM2 walks PPID, so it takes the still-attached script down with it, mid-delete and long before the closing `pm2 start`. The fingerprint was a partial deletion: only portos-server and portos-cos, the two entries declared before the script died, went missing. updateExecutor already solved this for the self-update route with spawnDetached's double-fork, so the PortOS record now delegates to executeUpdate() instead of keeping a second detached-spawn implementation in sync. appUpdater also skips its trailing restart step for that case, since update.sh starts the ecosystem itself. The delegation is narrowed to the PortOS record running this checkout's standard script: a custom updateCommand, or a repoPath pointing elsewhere, keeps the attached path so we never silently run a different script than the one configured. Non-PortOS managed apps are untouched. Defense in depth: both platform scripts now close with a `verify` step that polls /api/system/health until it reports ok, and on timeout spends one more `pm2 start` before logging the manual recovery command. Nothing previously noticed "I stopped the server and it did not come back" — the process that would have noticed is the one that did not come back. Removes appUpdater's dashboard handoff, which the delegation makes unreachable; update.sh runs open-ui-in-browser.js itself, with its own health-wait and navigate retry. Closes #5976
…5976) Review follow-ups on the App Management self-update path. The delegated launch now takes `setUpdateInProgress(true)` before starting update.sh. That flag is the same atomic lock POST /api/update/execute takes, so the two entry points can no longer launch the script concurrently — and it is what subAgentSpawner, agentLifecycle and persistentMindSupervisor gate on, so holding it also stops a CoS agent from being spawned into a process the script is about to `pm2 delete` (#4124). The route's remaining acknowledgement-bearing preflight guards need schema and UI work and are tracked in #5984. The `repoPath == PATHS.root` test that decides whether to delegate now resolves symlinks and case-folds on macOS/Windows. repoPath is user-editable and not force-synced, so a trailing slash or a different spelling of the same checkout would otherwise silently fall back to the attached spawn — re-arming the exact failure this fixes. The fallback is logged rather than silent. update.sh / update.ps1 no longer print "Update Complete" and exit 0 after the health verify failed twice. They print "Update applied, but PortOS is DOWN" and exit non-zero: the script outlives the server it restarts, so its exit status and the tail of data/update.log are the only signals a wrapper or an operator still has. Restores appUpdater's dashboard handoff. It is redundant for the delegated case, which update.sh handles, but a PortOS record with a custom updateCommand still restarts from here and would otherwise never reopen the dashboard. Also stops PORTOS_HEALTH_WAIT_MS=0 (and a typo'd value) collapsing into the 120s default, and drops a health-poll test assertion that pinned the loop's internal pass schedule rather than its timeout contract.
…path (#5976) The decline log fired even when no update routine ran at all, and reported 'a custom update command is configured' for a record that had none — sending an operator after the wrong misconfiguration. It now fires only where the routine actually runs, and distinguishes all three narrowings. Also corrects a docs sentence that credited non-PortOS managed apps with a dashboard handoff; that handoff opens the PortOS dashboard and returns early for every other app.
atomantic
force-pushed
the
claim/issue-5976
branch
from
September 3, 2026 05:08
4d29818 to
495d885
Compare
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.
Updating PortOS from the App Management page stopped
portos-serverand never brought it back, even when the pull was a no-op.Summary
update.shthroughappUpdater's plainspawn, notroutes/update.js.update.sh's ownpm2 delete ecosystem.config.cjstree-killsportos-server— and PM2 walks PPID, so it takes the still-attached script down with it, mid-delete and long before the closingpm2 start. The fingerprint was the partial deletion: onlyportos-serverandportos-cos, the two entries declared before the script died, went missing.updateExecutor.executeUpdate(), which already double-forks viaspawnDetachedfor the self-update route — rather than keeping a second detached-spawn implementation in sync.appUpdateralso skips its trailingrestartstep for that case, sinceupdate.shstarts the ecosystem itself.updateCommand, or arepoPaththat is not this checkout, keeps the attached path so we never silently run a different script than the one configured — and that decision is logged, since the attached path is the one that failed. The path comparison resolves symlinks and case-folds on macOS/Windows, becauserepoPathis user-editable and not force-synced. Non-PortOS managed apps are untouched.setUpdateInProgress(true)— the same atomic lockPOST /api/update/executetakes, so the two entry points cannot launchupdate.shconcurrently, and the flag CoS spawn gates read (Gate CoS spawn engines on updateInProgress to close self-update race #4124) is now up before the script deletesportos-cos.verifystep that polls/api/system/health(newscripts/verify-server-health.js) until it reports ok, and on timeout spends one morepm2 startbefore logging the manual recovery command. When it still fails, they print "Update applied, but PortOS is DOWN" and exit non-zero — the script outlives the server it restarts, so its exit status is the only signal a caller has. Nothing previously noticed "I stopped the server and it did not come back", which is what turned a no-op update into hours of downtime.Test plan
server/services/appUpdater.test.js— 15 tests: PortOS goes through the detached executor and neverbufferedSpawnOrThrow; the lock is taken before the launch and a second update is refused; therestartstep is skipped; a differently-spelledrepoPathstill delegates; a customupdateCommand, arepoPathoutside this checkout, and every non-PortOS app keep the attached spawn and their own restart + dashboard handoff; a failed update surfaces instead of reporting success.scripts/verify-server-health.test.js— 7 tests: URL ordering and dedupe, only a 200 reportingstatus: "ok"counts, a dead port fails, the poll/timeout contract on injected time, andPORTOS_HEALTH_WAIT_MSsentinel parsing.npm run lintclean inclient.update.shverified underbash -n.Deferred
POST /api/update/execute's remaining preflight refusals (live CoS agent, persistent-mind image work, fork acknowledgement) are still absent from the socket path. They needappUpdateSchemaand UI work to carry the acknowledgements, and are filed as #5984.Closes #5976