Repository navigation
control-plane: an offline node keeps its executions until it comes back - #1178
Merged
santoshkumarradha merged 1 commit intoNov 10, 2026
Merged
Conversation
A node that registers with metadata.custom.holds_executions_offline = true (the codeaf factory on a laptop that sleeps) no longer has its running work timed out by the stale sweep just because nothing renewed it while it was away. Before the sweeps run, each cleanup pass marks the quiet executions of an opted-in, registered node whose lease has lapsed as held: status stays running, status_reason is node_offline, and the stale, workflow-stale and stale-retry sweeps all skip it. A hold ends four ways. The node's next lease lists the execution as inflight: released, it carries on. The lease does not list it: failed now as node_lost_execution. The node is no longer registered: failed as node_removed. The hold outlives offline_hold_ceiling (default 168h, env AGENTFIELD_EXECUTION_OFFLINE_HOLD_CEILING): failed as node_offline_too_long. Nodes that did not opt in are reaped exactly as before, and an opted-in node that is online but stops listing an execution is still reaped. Assisted-by: CodeAF Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com>
santoshkumarradha
added a commit
to Agent-Field/CodeAF
that referenced
this pull request
Nov 10, 2026
The control plane keeps only deployment, performance and custom from a registration's metadata, so the factory's top-level codeaf key was dropped without a word. It now rides under custom, beside holds_executions_offline: true, which a control plane with Agent-Field/agentfield#1178 reads to keep the factory's executions running while its lease is expired (the engine down, the Mac asleep) instead of timing them out as stale, up to its offline ceiling. An older control plane ignores the key and keeps its stale timeout; the manual says both. Assisted-by: CodeAF Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com>
santoshkumarradha
merged commit Nov 10, 2026
fb8bb6a
into
proto/harness-watch
18 of 22 checks passed
santoshkumarradha
added a commit
to Agent-Field/CodeAF
that referenced
this pull request
Nov 10, 2026
* factory: an engine restart orphans nothing on AgentField The factory's AgentField node registered a fresh instance_id on every start. The control plane reads a re-registration under a different instance as "the previous process is gone" and, after its drain window, fails every execution still open under the old instance with agent_restart_orphaned. A factory's work is not in its process: the job is on the floor and the promise to report on it is in the side table, so an engine restart (a crash, the app relaunching it, a late key restarting the node) lost the caller's execution while the job went on and finished. The instance now belongs to the floor: made once and kept in agentfield-instance.json beside it, and registered by every start. A floor with open executions and no kept instance (the first start after this upgrade) adopts the instance the control plane holds for the node, so the upgrade itself orphans nothing. The fake control plane in the tests now reaps a changed instance the way the real one does. Assisted-by: CodeAF Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com> * factory: the registration says the factory holds its executions offline The control plane keeps only deployment, performance and custom from a registration's metadata, so the factory's top-level codeaf key was dropped without a word. It now rides under custom, beside holds_executions_offline: true, which a control plane with Agent-Field/agentfield#1178 reads to keep the factory's executions running while its lease is expired (the engine down, the Mac asleep) instead of timing them out as stale, up to its offline ceiling. An older control plane ignores the key and keeps its stale timeout; the manual says both. Assisted-by: CodeAF Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com> --------- Co-authored-by: CodeAF <267109073+agentfield-bot@users.noreply.github.com>
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.
Why
A node that is offline for a while (a laptop asleep overnight, an engine quit for hours) currently gets its running executions failed by the stale sweep. Nothing renews them while the node is away, so
ExecutionCleanupService.performCleanup(internal/handlers/execution_cleanup.go:179and:189) callsMarkStaleExecutions/MarkStaleWorkflowExecutions. Those select everyrunning|pending|queuedrow whoseupdated_atis older thanstale_execution_timeout(internal/storage/execution_records.go:1274and:1429) and set them totimeoutwith "execution timed out (no activity)". The only thing that keeps a row alive is the node's own lease listing it asinflight, and a sleeping node sends no lease. Agent-Field/CodeAF#2573 made the engine keep its instance id across restarts, so the restart reap no longer fires. This sweep was the remaining gap.What changes
The fix is opt-in per node. A node that registers with
metadata.custom.holds_executions_offline: truepromises that its work survives being offline, and that on its first lease back it lists every execution it still holds asinflight. Nodes that do not opt in behave exactly as before.running,status_reason = node_offline, andupdated_atrecords when the hold began (the UI can show "node offline since"). This is written toexecutionsandworkflow_executionstogether.MarkStaleExecutions,MarkStaleWorkflowExecutionsandRetryStaleWorkflowExecutionseach excludestatus_reason = 'node_offline'.node_lost_execution("node X came back without this execution: it was lost while the node was offline").node_removed.execution_cleanup.offline_hold_ceiling(default168h, envAGENTFIELD_EXECUTION_OFFLINE_HOLD_CEILING): it is failed withnode_offline_too_long.held_releasedandheld_lost.Notes
awaiting_agent_restart) is never re-held.status_reasonto NULL, so any earlier reason on a running row (for exampleapproval_granted) is not restored.main.Tests
The new tests are
internal/handlers/offline_hold_test.goandinternal/storage/offline_hold_test.go, run against real SQLite:succeededstatus report lands normally.node_lost_execution, with that message.node_removed.node_offline_too_long.Focused runs passed for
./internal/handlers(cleanup, lease, inflight, orphan and offline-hold tests),./internal/storage(stale, orphan and hold tests),./internal/config,./internal/server(config) and./internal/nodelink.Engine side
The engine needs a one-line change in
RegistrationFor(internal/factory/agentfield/registration.go). Add"custom": {"holds_executions_offline": true}undermetadata. The existingcodeafkey belongs incustomtoo, because the CP'sAgentMetadataonly keepsdeployment,performanceandcustom, so a top-levelcodeafkey is dropped today.