From 07d9c58e0fce47e354fbce9553debc18c6d9bfe5 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 24 Sep 2026 17:29:05 -0600 Subject: [PATCH 01/10] docs(deployment): document deployment.startupInstallTimeout Startup now waits at most deployment.startupInstallTimeout for component installs (HarperFast/harper#2784). Add the option, the 5.3 release note, and the new source of restartRequired in get_status. Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 4 +++- reference/operations-api/operations.md | 12 ++++++------ release-notes/v5-lincoln/5.3.md | 10 ++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index faf154e2f..b85bebf5d 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -406,16 +406,18 @@ agent: ## `deployment` -Retention for what `deploy_component` leaves behind. See [`deploy_component`](../operations-api/operations.md#deploy_component). +Retention for what `deploy_component` leaves behind, and how long startup waits for component installs. See [`deploy_component`](../operations-api/operations.md#deploy_component). ```yaml deployment: + startupInstallTimeout: 600000 payloadRetention: maxSize: 10485760 stagingRetention: maxCount: 5 ``` +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until it finishes, the node serves the version already installed, or nothing for a component that was never installed. When that install finishes, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status)). Harper does not restart on its own. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index f4042bfc9..caf592a5b 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1589,12 +1589,12 @@ With an `id`, it returns that one status record: { "operation": "get_status" } ``` -| Field | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `systemStatus` | Every status record set with `set_status` | -| `componentStatus` | Per-component health, aggregated across threads: `name` plus a `status` of `healthy`, `warning`, `error`, `loading`, or `unknown` | -| `restartRequired` | Whether a restart is pending on this node — set by a deploy that did not restart (see [Deploying without a restart](#deploying-without-a-restart)) and cleared on restart | -| `middlewareChains` | The resolved HTTP, upgrade, and WebSocket middleware order (v5.2.0). Present only when the request passes `middleware: true` | +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `systemStatus` | Every status record set with `set_status` | +| `componentStatus` | Per-component health, aggregated across threads: `name` plus a `status` of `healthy`, `warning`, `error`, `loading`, or `unknown` | +| `restartRequired` | Whether a restart is pending on this node — set by a deploy that did not restart (see [Deploying without a restart](#deploying-without-a-restart)) or by a component install that finished after startup stopped waiting for it (v5.3.0; see [`deployment.startupInstallTimeout`](../configuration/options.md#deployment)), and cleared on restart | +| `middlewareChains` | The resolved HTTP, upgrade, and WebSocket middleware order (v5.2.0). Present only when the request passes `middleware: true` | `restartRequired` is per node: it reports the node that served the request, not the cluster. diff --git a/release-notes/v5-lincoln/5.3.md b/release-notes/v5-lincoln/5.3.md index 756c7d336..618ccf57c 100644 --- a/release-notes/v5-lincoln/5.3.md +++ b/release-notes/v5-lincoln/5.3.md @@ -46,6 +46,16 @@ Keys already stored are not re-checked, and a value already sealed as `enc:v1:` Each SSH key's block in `/ssh/config` now runs from its `#` line through a `# END harper ssh key ` line, with a `# BEGIN harper ssh key ` line directly under the first. `get_ssh_key`, `list_ssh_keys` and `delete_ssh_key` read those lines instead of guessing where a block ends, so a line you add outside a key's lines is never removed with the key or read as its setting. Each node adds the lines to its existing config when it starts, without changing what ssh resolves, and a node rolled back to an earlier version still reads and deletes the blocks as it did before. See [The key's block in the ssh config](/reference/v5/operations-api/operations#the-keys-block-in-the-ssh-config). +### Bounded Startup Install Wait + +Startup no longer waits indefinitely for component installs. Listeners open only after startup's component installs finish, so one install that stalled — a git fetch that stops responding, a process stuck in I/O — kept the whole node down with nothing logged after the install began. Startup now waits at most `deployment.startupInstallTimeout` (default 10 minutes; `0` restores the unbounded wait) and logs the components it is still waiting for every 60 seconds ([harper#2072](https://github.com/HarperFast/harper/issues/2072)). + +An install still running at the deadline is not cancelled. It finishes in the background while the node serves the version already installed, or runs without the component if it was never installed. + +**During that window the node reports up while that application returns 404 or runs its previous version.** When the install finishes, `get_status` reports `restartRequired: true`. Restart the node to load it. + +**After upgrading, a node whose first install of a component takes longer than 10 minutes starts without that component.** Raise the timeout, or set it to `0` to keep the previous behavior. See [Configuration Options](/reference/v5/configuration/options#deployment). + ## Backups ### Subscriptions End When Their Database Is Restored (5.3.1) From a082cd74946eeb878f2e389e74d0318919148347 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sun, 27 Sep 2026 16:09:32 -0600 Subject: [PATCH 02/10] docs(release-notes): state the unbounded startup install wait as prior behavior The 5.3 note described listeners opening only after installs finish in the present tense, contradicting the timeout it introduces. Dispatch-Task: fix-kriszyp_documentation_683-f9f3e848 Co-Authored-By: Claude Opus 5.5 --- release-notes/v5-lincoln/5.3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/v5-lincoln/5.3.md b/release-notes/v5-lincoln/5.3.md index 618ccf57c..058ad3443 100644 --- a/release-notes/v5-lincoln/5.3.md +++ b/release-notes/v5-lincoln/5.3.md @@ -48,7 +48,7 @@ Each SSH key's block in `/ssh/config` now runs from its `#` line ### Bounded Startup Install Wait -Startup no longer waits indefinitely for component installs. Listeners open only after startup's component installs finish, so one install that stalled — a git fetch that stops responding, a process stuck in I/O — kept the whole node down with nothing logged after the install began. Startup now waits at most `deployment.startupInstallTimeout` (default 10 minutes; `0` restores the unbounded wait) and logs the components it is still waiting for every 60 seconds ([harper#2072](https://github.com/HarperFast/harper/issues/2072)). +Startup no longer waits indefinitely for component installs. Previously, listeners opened only after every startup component install finished, so one install that stalled — a git fetch that stopped responding, a process stuck in I/O — kept the whole node down with nothing logged after the install began. Startup now waits at most `deployment.startupInstallTimeout` (default 10 minutes; `0` restores the unbounded wait) and logs the components it is still waiting for every 60 seconds ([harper#2072](https://github.com/HarperFast/harper/issues/2072)). An install still running at the deadline is not cancelled. It finishes in the background while the node serves the version already installed, or runs without the component if it was never installed. From 034be55c15ef595536b04574efa44284ec672e50 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sun, 27 Sep 2026 16:15:11 -0600 Subject: [PATCH 03/10] docs(deployment): restartRequired follows a late install's success, not its failure A late install that fails sets no flag and is only logged; say so, and note that invalid startupInstallTimeout values fall back to the default. Dispatch-Task: fix-kriszyp_documentation_683-f9f3e848 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- reference/operations-api/operations.md | 12 ++++++------ release-notes/v5-lincoln/5.3.md | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index b85bebf5d..2fad9ace4 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until it finishes, the node serves the version already installed, or nothing for a component that was never installed. When that install finishes, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status)). Harper does not restart on its own. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When the install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status)). Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs the error. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0. A negative or non-numeric value falls back to the default, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index caf592a5b..ed9c09ed2 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1589,12 +1589,12 @@ With an `id`, it returns that one status record: { "operation": "get_status" } ``` -| Field | Description | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `systemStatus` | Every status record set with `set_status` | -| `componentStatus` | Per-component health, aggregated across threads: `name` plus a `status` of `healthy`, `warning`, `error`, `loading`, or `unknown` | -| `restartRequired` | Whether a restart is pending on this node — set by a deploy that did not restart (see [Deploying without a restart](#deploying-without-a-restart)) or by a component install that finished after startup stopped waiting for it (v5.3.0; see [`deployment.startupInstallTimeout`](../configuration/options.md#deployment)), and cleared on restart | -| `middlewareChains` | The resolved HTTP, upgrade, and WebSocket middleware order (v5.2.0). Present only when the request passes `middleware: true` | +| Field | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `systemStatus` | Every status record set with `set_status` | +| `componentStatus` | Per-component health, aggregated across threads: `name` plus a `status` of `healthy`, `warning`, `error`, `loading`, or `unknown` | +| `restartRequired` | Whether a restart is pending on this node — set by a deploy that did not restart (see [Deploying without a restart](#deploying-without-a-restart)) or by a component install that succeeded after startup stopped waiting for it (v5.3.0; see [`deployment.startupInstallTimeout`](../configuration/options.md#deployment)), and cleared on restart | +| `middlewareChains` | The resolved HTTP, upgrade, and WebSocket middleware order (v5.2.0). Present only when the request passes `middleware: true` | `restartRequired` is per node: it reports the node that served the request, not the cluster. diff --git a/release-notes/v5-lincoln/5.3.md b/release-notes/v5-lincoln/5.3.md index 058ad3443..b0df8898e 100644 --- a/release-notes/v5-lincoln/5.3.md +++ b/release-notes/v5-lincoln/5.3.md @@ -50,9 +50,9 @@ Each SSH key's block in `/ssh/config` now runs from its `#` line Startup no longer waits indefinitely for component installs. Previously, listeners opened only after every startup component install finished, so one install that stalled — a git fetch that stopped responding, a process stuck in I/O — kept the whole node down with nothing logged after the install began. Startup now waits at most `deployment.startupInstallTimeout` (default 10 minutes; `0` restores the unbounded wait) and logs the components it is still waiting for every 60 seconds ([harper#2072](https://github.com/HarperFast/harper/issues/2072)). -An install still running at the deadline is not cancelled. It finishes in the background while the node serves the version already installed, or runs without the component if it was never installed. +An install still running at the deadline is not cancelled. It keeps running in the background while the node serves the version already installed, or runs without the component if it was never installed. -**During that window the node reports up while that application returns 404 or runs its previous version.** When the install finishes, `get_status` reports `restartRequired: true`. Restart the node to load it. +**During that window the node reports up while that application returns 404 or runs its previous version.** When the install succeeds, `get_status` called with no `id` reports `restartRequired: true`. Restart the node to load it. An install that fails after the deadline does not set the flag; Harper only logs the error. **After upgrading, a node whose first install of a component takes longer than 10 minutes starts without that component.** Raise the timeout, or set it to `0` to keep the previous behavior. See [Configuration Options](/reference/v5/configuration/options#deployment). From c87adca7c170f6bfc4515d4f97b31be7332164ab Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sun, 27 Sep 2026 16:18:49 -0600 Subject: [PATCH 04/10] docs(deployment): name get_status's no-id form and the silent empty-value default Dispatch-Task: fix-kriszyp_documentation_683-f9f3e848 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 2fad9ace4..82da715f2 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When the install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status)). Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs the error. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0. A negative or non-numeric value falls back to the default, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When the install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`). Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs the error. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` From a1f0cf7f1cb38842335992b50016faf059689f16 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 09:59:27 -0600 Subject: [PATCH 05/10] docs(deployment): cross-reference the startup-install restart source, log-interval floor, and any-success rule Addresses pre-push review nits: link the "Deploying without a restart" section to the new startup-timeout restartRequired source, note the 60s progress log doesn't fire before a sub-minute deadline, and state that any one component's late success sets the flag. Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- reference/operations-api/operations.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 82da715f2..f49b72758 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When the install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`). Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs the error. Startup logs the components it is still waiting for every 60 seconds. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging which ones it is still waiting for every 60 seconds (a timeout under a minute skips that log; nothing logs before the deadline). A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When an install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index ed9c09ed2..3e06cad12 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1098,6 +1098,8 @@ Two kinds of deploy set it: - A component that had no directory before this deploy. It has never been loaded, so its routes cannot be live until Harper restarts. - A redeploy whose package metadata changed (v5.2.1) — a dependency or module-entry change invalidates loaded code, and package metadata sits outside most plugin file globs, so the component's own watcher does not see it. +A third source is not a deploy at all: a component install still running when [`deployment.startupInstallTimeout`](../configuration/options.md#deployment) expires at startup sets the flag if it later succeeds (v5.3.0). + An ordinary redeploy sets nothing: the component's watched files are handled by its file watcher, which requests a restart only when the update needs one. The flag is evaluated per node. A peer applying the replicated deploy checks its own directory state, since whether the component was already active can differ from node to node. From 17c7e10117a9f76852d4b3792421d08f313f3b52 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 10:03:04 -0600 Subject: [PATCH 06/10] docs(deployment): separate the progress log from immediate failure logging The 60s "still waiting" log and an install failure are different log lines with different timing; "nothing logs before the deadline" wrongly implied a failure is silent until the deadline too. Verified against harper#2784: installConfiguredApplication's catch logs "Failed to prepare application " the moment the promise rejects, independent of the progress timer. Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index f49b72758..49c35a664 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging which ones it is still waiting for every 60 seconds (a timeout under a minute skips that log; nothing logs before the deadline). A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When an install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for (skipped when the timeout is under a minute); an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When an install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` From 24d84b70f2b303aaf361f731388e36288847cebb Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 10:12:34 -0600 Subject: [PATCH 07/10] docs(deployment): a late install's swap lands on disk before the restart "It serves the version already installed" overclaimed: the candidate tree is renamed onto the live path inside prepareApplication as part of a successful install, before restartRequired is even set. Loaded JS stays the old generation until restart, but anything served fresh from disk (server/static.ts reads staticFile by realpath per request) reflects the new build immediately. Verified against harper#2784 (Application.ts: prepareApplication -> activateCandidateApplication renames the candidate into place before the preparation promise resolves). Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 49c35a664..5ca9c9232 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for (skipped when the timeout is under a minute); an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background. Until the node restarts after that install succeeds, it serves the version already installed, or nothing for a component that was never installed. When an install succeeds, Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for (skipped when the timeout is under a minute); an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart; only content served straight from disk, such as static files, can reflect the new version before then. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` From 054b05c05690f59787ee7afe0239bd36ab52814a Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 10:16:54 -0600 Subject: [PATCH 08/10] docs(deployment): 0's unbounded wait keeps logging every 60s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "skipped when the timeout is under a minute" read as covering 0 too. It doesn't: installApplications() passes timeoutMs === 0 ? Infinity : timeoutMs to waitForStartupPreparations, and Number.isFinite(Infinity) is false, so no deadline timer is set — the independent 60s progressTimer keeps firing for the whole unbounded wait. Only a positive, finite timeout under 60000ms lets the deadline win before the first tick. Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 5ca9c9232..612d046d0 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for (skipped when the timeout is under a minute); an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart; only content served straight from disk, such as static files, can reflect the new version before then. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for — including throughout `0`'s unbounded wait; only a positive timeout under a minute ends the wait before that first log — and an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart; only content served straight from disk, such as static files, can reflect the new version before then. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` From 1fa016b285e20d72b46364cb2f2e9dad5af2019a Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 10:26:47 -0600 Subject: [PATCH 09/10] docs(deployment): jsResource add-file live-load, harper dev auto-restart, wait-ends-early wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more findings the domain adjudicator kept as in-scope: - "only content served straight from disk... can reflect the new version" was wrong: jsResource's `add` handler (resources/jsResource.ts) imports and registers a genuinely new file live, no restart. Only a changed/unlinked file requests one. - "Harper does not restart on its own" is false under `harper dev`: componentLoader.ts sets autoReload from DEV_MODE and subscribes to restart requests, auto-restarting workers 500ms after any requestRestart() call, including the startup-timeout one. - "only a positive timeout under a minute ends the wait before that first log" was backwards: waitForStartupPreparations races all installs settling against the deadline, so any install set that finishes within 60s skips the first log regardless of the timeout value, `0` included. Also stopped calling a negative number "non-numeric" — it parses fine, it just fails the >= 0 check. Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 612d046d0..815780554 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners, logging every 60 seconds which ones it is still waiting for — including throughout `0`'s unbounded wait; only a positive timeout under a minute ends the wait before that first log — and an install that fails logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart; only content served straight from disk, such as static files, can reflect the new version before then. Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative or other non-numeric value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. While waiting, it logs every 60 seconds which ones remain outstanding — including throughout `0`'s unbounded wait — unless every install settles first, which always ends the wait before that first log, whatever the timeout; a failed install logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart, except that a file the swap newly adds — not one it changes — can load live if the component reacts to added files on its own, as the built-in JS resource handler does; content served straight from disk, such as static files, can also reflect the new version before the restart. Outside `harper dev`, which restarts automatically soon after any restart request, Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative, non-finite, or otherwise unparseable value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5` From 7a694cd00274acc4a4d8ce6a925b11a4971b1e88 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 29 Sep 2026 10:30:17 -0600 Subject: [PATCH 10/10] docs(deployment): generalize the plugin-live-apply claim instead of enumerating mechanics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "not one it changes" was wrong: roles.ts and dataLoader.ts apply both add and change events, only skipping unlink — contradicting the add-only claim I'd just made for jsResource. Enumerating which plugin does what goes stale and keeps being wrong in a new specific way each time; state the fact (some plugins apply live) without the mechanics. Dispatch-Task: pr-maint-680758f7429b14f36588bf99a2c09c3c Co-Authored-By: Claude Sonnet 5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 815780554..bb0b5d1df 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -417,7 +417,7 @@ deployment: maxCount: 5 ``` -- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. While waiting, it logs every 60 seconds which ones remain outstanding — including throughout `0`'s unbounded wait — unless every install settles first, which always ends the wait before that first log, whatever the timeout; a failed install logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart, except that a file the swap newly adds — not one it changes — can load live if the component reacts to added files on its own, as the built-in JS resource handler does; content served straight from disk, such as static files, can also reflect the new version before the restart. Outside `harper dev`, which restarts automatically soon after any restart request, Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative, non-finite, or otherwise unparseable value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) +- `startupInstallTimeout` — Milliseconds. The longest startup waits for component installs before it opens listeners. While waiting, it logs every 60 seconds which ones remain outstanding — including throughout `0`'s unbounded wait — unless every install settles first, which always ends the wait before that first log, whatever the timeout; a failed install logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (`restartRequired` in [`get_status`](../operations-api/operations.md#set_status--get_status--clear_status) called with no `id`) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart, but some component plugins apply an added or changed file on their own rather than waiting for one; content served straight from disk, such as static files, can also reflect the new version before the restart. Outside `harper dev`, which restarts automatically soon after any restart request, Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component. `0` waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative, non-finite, or otherwise unparseable value also falls back to it, with a warning in the log; _Default_: `600000` (10 minutes) - `payloadRetention.maxSize` — Bytes. After a successful deploy, a payload larger than this has its stored tarball (`payload_blob`) dropped from the `hdb_deployment` row; the row and its metadata stay. Set it very high to retain every payload; _Default_: `10485760` (10 MiB) - `stagingRetention.maxCount` — How many complete builds that are not live may remain per component under `/.deploy-staging`: builds staged with `activate: false`, and [releases replaced by a later deploy](../operations-api/operations.md#going-back-to-a-previous-release), which `deployment_id` can make live again. The newest survive; older ones are removed when that component next deploys and at startup. `drop_component` removes all of them. Each is a full installed copy of the component, `node_modules` included. `0` keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; _Default_: `5`