Repository navigation
Track rolling deploys per node, report releases and deploy provenance, and expire failed payloads - #3502
Conversation
…in-band sentinel event DeploymentRecorder.finish() emitted _recorder_finished on the deploy's progress emitter before unsubscribing, so the sentinel landed in event_log, a streamed deploy_component and the get_deployment tail. The registry now carries a promise finish() settles after the terminal write; the tail awaits it (or its client's disconnect) and re-reads the final row. Fixes #3062 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pgraded installs An upgraded install keeps the catalog each system table was created with, so attributes added to systemSchema.json later (activated_from, install_fingerprint on hdb_deployment) never reached it. The 5.4.0 directive runs registerSystemSchemaAttributes, which adds whatever the schema declares and the catalog lacks, indexed as a fresh install indexes it; each later release's directive can list it again. mount_hdb and the directives now hand table() a copy of each schema attribute list, since table() kept the bundled schema's array as the live attribute list. Fixes #2659 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every get_components entry now carries deployment_id (the id the live tree's provenance marker names), deployed_at (completed_at of the hdb_deployment record that made that release live) and restarted_at (the latest time a thread on this node finished loading the application, kept on ComponentStatus and aggregated across threads). hdb_deployment stays the only record of deploys. Fixes #564 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oyment record A certified rolling deploy's record was finished as success before its activation job ran, so get_deployment and list_deployments reported success for a release a peer then rejected. The origin now finishes the record as rolling_out with the job's id in restart_job_id; the job adds each peer's activation (activated, certification, message, error) to its peer_results entry and ends the record success, or partial naming the nodes that did not take the release. The job waits for the origin's rolling_out write before its first, and carries the last peer's outcome in its final write so the two never land as a tight burst. get_components treats rolling_out and partial records as released. Fixes #3060 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e release When a rolling deploy answers rolling_out, the CLI follows its deployment record with get_deployment on the same connection and credential, printing each node's outcome as it lands, and exits non-zero on partial, failed or a timeout (ten minutes per node by default, wait_timeout= to override), naming the nodes that did not take it. wait=false keeps the old exit. The record replicates, so the wait works behind a load balancer where a node-local get_job poll does not; the GitHub Actions CI role now grants get_deployment. Fixes #3058 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
list_releases { project?, replicated? } reports, per component on this node, the deployment_id
the live tree's provenance marker names and every dormant build deploy_component deployment_id
can activate, each with whether it was staged or kept (replaced here), when, and its size on disk.
With replicated: true it returns every node's answer. An activation that keeps the release it
displaced now writes 'kept' into that record's .complete marker, which nothing that settles or
retains a build reads.
Fixes #3059
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… policy
The OIDC exchange now signs the policy id and the run's verified claims (repository, sha, ref,
workflow_ref, run_id, run_attempt, actor, environment) into the operation token it mints, and
deploy_component records them on the deployment row as source: { via: 'oidc', ... }. A deploy
that does not come through OIDC records what the CLI declares (the GitHub Actions run, and for a
directory deploy the checkout's commit and a dirty flag) as via: 'declared'; an attested source
always wins. get_deployment and list_deployments return source, and list_deployments filters by
sha.
Fixes #3061
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before the swap, deploy_component checks the built tree for every jsResource and graphqlSchema files pattern the component's own config file declares, and fails in the prepare phase naming each one that matched nothing, so a truncated or partial payload no longer reports success while the release it replaced keeps serving. The deploy_component unit tests for #3061 now read the per-run components root deploy_component actually uses. Refs #1719 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed deploy keeps its payload tarball for retry and debugging, so a retried rollout used to accumulate one copy per attempt forever. sweepFailedPayloads now drops, per project, every failed payload beyond the newest deployment_payloadRetention_failedMaxCount (default 2) and every one older than deployment_payloadRetention_failedMaxAge (default 7d), never the newest; 0 turns a bound off. It writes the row with payload_blob null, as delete_deployment_payload does, so peers drop their copies, and only for rows this node wrote. It runs after a deploy failure is recorded and once per process at boot. Fixes #2681 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n after the response A part piped after the response that loses its destination, to a destination error or unpipe(), is left paused but not destroyed, so nothing re-entered the discard and a client that kept sending stalled until the request timeout. While a handed-out part is held after the response, the parser now re-checks it whenever it loses a data or readable listener, which Node's pipe cleanup removes in both cases. Fixes #3096 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t CLI test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A config that names a file its release no longer ships is a legal, resource-less component, and redeploying to delete an application's only resource file is a tested workflow (redeploy-restart-flag-deletion.test.ts), which the check failed. It now names each missing jsResource/graphqlSchema pattern in a warning event and in the response message, and the deploy proceeds. Refs #1719 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lank settings - get_components selects only the four fields it reads from hdb_deployment rather than copying every released row's event_log. - The activation job keeps at least a second between its writes to the row, counting the origin's own rolling_out write, so a peer that fails at once cannot produce the burst #1170 describes. - A recorder's tail that arrives during the terminal write waits for it, and forwards every event after its replay. - A blank or null failedMaxCount/failedMaxAge reads as unset, not as 0. - A negated entry-point pattern is read as an exclusion. - harper deploy validates wait_timeout before sending, and the rollout bound grows once a read says how many nodes there are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…releases replication - harper deploy keeps waiting until the record ends; a node whose copy of the row lags shows the origin's earlier status, which is no ending. - list_releases settles the replication promise at once, so a rejection during the local read is never unhandled, and raises it after. - deploymentSource keeps its internal helpers unexported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load bound - get_components finds the record of each live release by key and by the activated_from index instead of scanning every released row on every poll. - Failed payloads are ranked across every node's rows, so the bound is per project, not per origin; each node still drops only the rows it wrote. - The activation job waits up to ten minutes for the origin's rolling_out write, since an origin write landing after the job's would erase the job's outcomes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… comment history - A component without a provenance marker finds its newest released record by walking its project's rows newest first and stopping at the first, instead of reading its whole history. Checked against the real deployment table on both engines. - The entry-point check skips a non-string pattern rather than throwing, and reads a leading / on a negation as the component root, as on a positive pattern. - New comments drop issue-number provenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… each rollout poll - When the origin writes a rolling deploy's rolling_out row, it keeps the peer activations and final status the activation job already stored, so the hand-off no longer depends on the job's wait outlasting the origin. - Each get_deployment poll is bounded by what is left of the CLI's wait. - get_components falls back to a failed record that names the live release, for a deploy that swapped here and then failed on a peer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loyment record notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge units
- At boot, after interrupted jobs are settled, each rolling_out record this node wrote whose
activation job is no longer running is ended: success only when the job completed and every peer
took the release, otherwise partial naming the unconfirmed peers.
- The activation job waits for the origin's write once per row, so a write that never landed
delays only the first activation.
- failedMaxAge accepts only convertToMS's units; another suffix used to read as seconds.
- A refused rollout poll names both remedies, since an expired token also answers 403.
- The multipart watch reacts only to a lost data listener; a reader looping on
once('readable') holds none between reads.
- A declared source's dirty flag counts only changes inside the deployed directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the markerless walk - The activation job's final write ends a row still short of the origin's own write, which the origin's late write then keeps; only a row the origin ended some other way keeps its status. - A component without a provenance marker reads at most 100 of its project's rows, newest first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…off across a failed write - failedMaxAge is trimmed before both its check and its conversion; ' 7d ' read as seven seconds, since convertToMS takes the last character as the unit. Y is accepted too. - The activation job's wait for the origin's write is over once it has run, whether the job's next write landed or not. - An incomplete boot pass of the rollout settlement or the failed-payload sweep is not remembered, so the next root reload tries again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements a rollout tracking mechanism for rolling deployments, introduces the list_releases operation to report live, staged, and kept releases on disk, and enhances get_components with deployment and worker load times. It also adds failed payload retention to expire old failed deployment tarballs and records deployment sources (attested via OIDC or declared by the client). The review feedback suggests parallelizing independent I/O operations in keptReleasesOf and releasesOnThisNode using Promise.all to improve performance. Additionally, it recommends adding defensive nullish checks and optional chaining to prevent potential TypeError crashes, and resolving TypeScript compilation errors related to handling caught errors of unknown type in deployRollout.ts.
…o reads Tighten this branch's components/DESIGN.md notes so the file is back under its 1,000-line budget (check:design-docs). keepRolloutProgress now skips a stored peer entry with no node, and get_components' debug log tolerates a non-Error rejection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d drop a deadline-cut read from the timeout message A certified restart=rolling deploy activated on no peer on a real cluster: its restart_service job runs in a job worker thread, whose server.nodes is empty until something subscribes to node updates, and monitorNodeCAs (which does) ran only after the peers had been chosen from that empty list. Every peer was reported as no longer in the cluster. The job now subscribes before reading the list; on Harper core alone, whose stub throws, there are no peers. A 3-node harper-pro cluster run of the simulation found it. The CLI's rollout wait also stopped reporting "last read failed" for the final read that the end of the wait itself cut short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded, not only core's stub The catch around monitorNodeCAs swallowed every failure, so a harper-pro scan of hdb_nodes that threw would leave server.nodes empty and an activation with no node list would record success having visited no peer. Core's replication stand-ins now throw with code REPLICATION_NOT_IMPLEMENTED, and only that means "no peers". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…step fits its time limit The Node 22 "Unit tests: main" step ran past its 8-minute limit (8:12; main's own runs take 7:20 to 7:48). Each test of the rollout-record suite waited out the job's one-second spacing between row writes, 13 seconds in all. The spacing is now rolloutTiming, which that suite shortens to 50 ms; the spacing test itself still waits 400 ms and fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Takes #3523's DESIGN.md index, which shares closely related entries on one line with a `·` separator, and adds this branch's two index changes in that form: the deployment-record entry beside the install-fingerprint one, and the entry-point warning in the dangling-symlink description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #562's type-strip mode, which turned components/operations.js and server/loadRootComponents.js into .ts with static imports. This branch's changes to them now use static imports too (deploymentSource, declaredEntryPoints, payloadRetention, deploymentOfLiveRelease, liveDeploymentIdOf, and the two boot sweeps), and get_components' record lookup returns undefined from its catch so it typechecks. restart.ts keeps this branch's node-list load with main's .ts import. components/DESIGN.md is trimmed back under its line budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runs can load it upgrade/systemSchemaAttributes.ts imported json/systemSchema.json directly. Since #562, Harper and its native unit runs load .ts sources as ESM, where a JSON import needs `with { type: 'json' }`, so every type-strip boot and native unit run failed with ERR_IMPORT_ATTRIBUTE_MISSING. It now takes the schema from the CommonJS wrapper main's directives use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de reports no other way Native Unit Test on Node 22.18.0 and 24.0.0 failed the harper#3096 cases where a part piped after the response loses its destination. On those versions a pipe's cleanup removes the part's `data` listener without emitting `removeListener`, and a part already paused by backpressure emits no `pause`, so nothing prompted the abandonment check. Every lost pipe ends in the part's own unpipe() (the route's, or the cleanup's after its destination errors or closes), so the watch now also looks after that call, and restores the method once the part ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tcomes a lost write drops Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and find a markerless one past unreleased deploys Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t idles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_releases Documents HarperFast/harper#3502: a certified rolling deploy's record follows its job (rolling_out, then success or partial, with each node's activation), harper deploy restart=rolling waits on it (wait=false, wait_timeout=), the CI role and guide gain get_deployment, list_releases, get_components' deployment_id/deployed_at/restarted_at, the record's source and list_deployments sha, failed-payload retention, and the warning for a declared entry point that matched nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_releases Documents HarperFast/harper#3502: a certified rolling deploy's record follows its job (rolling_out, then success or partial, with each node's activation), harper deploy restart=rolling waits on it (wait=false, wait_timeout=), the CI role and guide gain get_deployment, list_releases, get_components' deployment_id/deployed_at/restarted_at, the record's source and list_deployments sha, failed-payload retention, and the warning for a declared entry point that matched nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
⊙ Problem
Deployment tracking (#641) left gaps. A rolling deploy's record, and
harper deploy restart=rolling, reported success before other nodes took the release. Nothing reported deploy or restart times, a node's kept releases, or a deploy's origin. Upgraded installs missed newerhdb_deploymentattributes. A truncated payload could report success. Failed payloads were kept forever. An internal event leaked into deploy history.🤔 Does all of this belong in one PR?
Asked for as one PR, one commit per issue, so each reads and reverts on its own. The commits share
components/deploymentRecorder.tsandcomponents/DESIGN.md, which is why they were not split.💡 Solution
rolling_outuntil every node took the release, then endssuccessorpartialnaming the nodes that did not.harper deploywaits on that record.get_componentsgives each applicationdeployment_id,deployed_atandrestarted_at; newlist_releaseslists each node's releases.source, attested by OIDC or declared by the CLI.jsResource/graphqlSchemaentry point is missing.🤔 Should a missing entry point fail the deploy (#1719)?
It warns, so #1719 stays open. Failing broke a tested workflow: redeploying to delete an application's only resource file, whose config still names it (
redeploy-restart-flag-deletion). Making it fatal, ending that workflow, is one line.⚖️ Alternatives
hdb_deploymentalready records every deploy.harper deploy restart=rollingexits 0 before the other nodes have taken the release #3058 by pollingget_job—hdb_jobdoes not replicate, so a poll behind a load balancer can land on a node that never had the job.get_componentsfields — sizing each kept tree walks it on everyget_componentspoll;list_releasespays that only when asked.pauseorremoveListeneralone — backpressure and Node 24.0/22.18 each miss one; every lost pipe calls the part'sunpipe().🔧 Changes
Left for later: #1719's fatal check (above); an uncertified
restart=rollingstill recordssuccessbefore its peers restart, and the CLI does not wait on it; a payload's install fingerprint still has no source (#3061); a failed payload whose node left the cluster is never expired; a release kept before 5.4 reportsstaged. Docs need a companion PR in HarperFast/documentation.🤔 Should the rollout's state live on the deployment row?
Yes:
get_deploymentandlist_deploymentsalready read the row, and it replicates. The cost is write ordering, handled by the job's wait and spacing and a node-scoped lock on the row. Separate per-peer records would avoid contention but give readers two places to look.🤔 Should
harper deploy restart=rollingwait by default?Yes: #3058 asks that CI not pass before every node took the release. A CI step that exited at the origin's swap now waits up to ten minutes per node and can fail;
wait=falsekeeps the old exit.🤔 Should
restarted_atcount a load that failed?Yes: it says when this node's workers last loaded the application;
statussays whether that worked. Renaming it later would break callers.🤔 Should
list_deployments { sha }match declared sources?Yes: each row's
source.viasays whether a CI exchange attested it. Matching only attested sources would hide every deploy from a laptop.Per issue: what changed, with links
#3062, the recorder's internal event.
finish()no longer emits_recorder_finished; the registry of active deploys holds a promise it settles after the terminal write. The registry entry is removed only after that write. Theget_deploymenttail awaits it, or its client's disconnect, and re-reads the final row.#2659, attributes on upgraded installs.
registerSystemSchemaAttributesadds every attributesystemSchema.jsondeclares that an existing system table lacks, indexed like a fresh install. The 5.4.0 directive runs it (registered).mount_hdband the 5.1, 5.2 and 5.3 directives now passtable()a copy of the schema's attribute list, sincetable()kept the bundled array as the live list. The rule is in dataLayer/DESIGN.md;systemSchema.jsongainsrestart_job_idandsource.#564, deploy and restart times.
getComponentsreads the live marker (liveDeploymentIdOf) and finds the record that made it live by key and by theactivated_fromindex, not a scan of the history; a catalog that cannot search that index yet keeps the record found by key. A tree with no marker takes the newest row of each released status, so no run of failed attempts hides it.restarted_atcomes fromloadedAt, set when a thread finishes loading, kept across reports, aggregated across threads and sub-components (types).#3060, peer outcomes on the record. The origin finishes a rolling deploy as⚠️ Look hardest: the job waits for the origin's
rolling_outwithrestart_job_id. The activation job records each peer and ends the recordsuccessorpartial. The origin's own write keeps what the job already stored, and a boot ends records whose job was interrupted (at boot).rolling_outwrite before its first; the last peer's outcome, and any whose own write failed, rides the final write and every write waits a second after the previous one, so writes to the row never land as a burst (#1170). Each read-merge-write, the origin's and the job's, holds the row's node-scoped lock, since a conflict retry replays a put without rerunning its merge (LMDB has none). The final write retries, then fails the job with every outcome in its record, which the boot pass reads back.partialis terminal. The job loads the cluster's node list itself: a job worker thread starts with an emptyserver.nodes, so until this branch a certified rolling deploy activated on no peer of a real cluster, onmaintoo. Only core's replication stand-in means no peers, and a job that fails before it can finish the record ends itpartial(abandonRollout).#3058, the CLI waits.
awaitRolloutpollsget_deploymentwith backoff, prints each node's outcome, ends only on an ending status, since a lagging node still shows the origin's earlier one, stops at once on a refusal and times out at ten minutes per node.followRolloutreuses the deploy's connection and token, ends each read at the wait's deadline however slowly its reply trickles, and the CLI exits non-zero;wait/wait_timeoutare kept off the wire and checked first. A read the wait's own end cut short is not reported as a failed read. The CI role grantsget_deployment.#3059, releases per node.
list_releases(replication settled at once) reports each component's live id and kept releases with state, time and size, over every component with a tree or a build;replicated: trueadds each node's answer. A displaced release is marked kept in its.completecontent. Registered in hdbTerms, serverUtilities (import), operation_authorization (import) and the MCP schema and description.#3061, where a deploy came from. The OIDC exchange signs the policy and run claims into the token (
createOperationToken), validation lifts it onto the principal (claim), and impersonation carries it.deploymentSourcepicks attested over declared and forces a declared one to say so;deployComponentrecords it on the row. The CLI declares the Actions run and the checkout commit (wired).list_deploymentsfilters bysha.#1719, entry points.
missingEntryPointsWarningchecks each declared pattern, reading negations as exclusions, and its glob walk, with the loader'sonly:filters, stops at the first match. It runs in the deploy'svalidateCandidate, before the swap, and the response message carries the warning.#2681, failed payloads.
sweepFailedPayloadsranks every node's failed rows per project but drops only its own, applies the bounds (config names), runs after a failure is recorded and once at boot.#3096, an upload whose pipe loses its destination.
watchForAbandonmentre-checks a part held after the response after its ownunpipe()or when it loses adatalistener, armed from the response hook.Design notes. components/DESIGN.md gains a section on the deployment record (each item above) and one on entry points; also server/DESIGN.md, security/DESIGN.md and the index.
Docs. HarperFast/documentation#751 documents these changes for 5.4.
✅ Verification
deploy-trackingchecks Track per-application deployment and restart timestamps #564 and Record where a deploy came from: the commit, the CI run and the trust policy #3061;canary-rolloutchecks therolling_out→successrecord and runsharper deploy restart=rollingto exit 0;deploy-setup-github-actionschecks the role.minor-upgradegains a check that turns on oncepackage.jsonreaches 5.4.mainbuilt on this branch: every peer takes a certified rolling release, and a peer whose canary rejects ends the recordpartialnaming it while the others serve it. Before the activation fix, no peer did. Rerun at5c2d664: unchanged.Three-node scenarios and gates
wait_timeout,wait=false, an origin killed mid-rollout (settledpartialat its restart), the entry-point warning,list_releases, per-nodeget_components, theshafilter and failed-payload expiry all behave as documented.5c2d664: affected suites pass on dist, LMDB and type-stripped source.bin,componentsandserverHelpersunit suites pass (3,100;OptionsWatcherfails on base here too), andcanary-rolloutpasses 11 of 11.test:unit:main7707 passing; its 3 failures pass alone or fail on base too.integrationTests/deploy,componentsandupgraderan in batches: onlyshutdown-drain-e2efailed, as on base. Type check andlint:requiredpass.Unit tests by issue
_recorder_finishedevent leaks intoevent_logand SSE streams #3062: recorder, tail.get_components, a failing search and a long unreleased run.successeven when another node rejects the release #3060: peer outcomes on the row, a final write that cannot land, the lock (on a real table), boot recovery from the job.harper deploy restart=rollingexits 0 before the other nodes have taken the release #3058:awaitRollout, CLI, a trickling read (httpRequest).deploy_component, CLI.deploy_component,only:filters.unpipe().Fixes #3062
Fixes #2659
Fixes #564
Fixes #3060
Fixes #3058
Fixes #3059
Fixes #3061
Fixes #2681
Fixes #3096
Refs #1719
🤖 Generated by Claude Code (Claude Opus 5.5); posted via @dawsontoth.
Release target: main — new operations, fields and CLI behavior for v5.4
Related PRs: #372 overlaps (merged into this branch; its status-registry rework now carries
loadedAtacross a level change), #3360 overlaps (both add a 5.4.0 directive and editoperation_authorization.ts; whichever lands second merges the directives), #3498 overlaps (merged into this branch; it restarts each peer in turn for an uncertified rollout, this activates each peer for the certified one), #3339 overlaps (merged into this branch; both editdeployComponentandbin/restart.ts), #3131 overlaps (both add tests tounitTests/components/deploymentRecorder.test.js; whichever lands second merges them), #3523 overlaps (merged into this branch; both edit the rootDESIGN.mdindex), 21 others independentComplexity: complicated
Deferred findings
f07ebe9f80c92088minor data-integrity —resources/Table.ts— replica admission and demotion drop receivedIdentityRefs, weakening duplicate-replay protection — filed Replica admission and demotion drop a row's receivedIdentityRefs #3531f6003a95384597a3nit reliability —components/deploymentOperations.ts— same-millisecond buffered live event dropped during replay — filed get_deployment's live tail can drop an event emitted in the same millisecond as the last replayed entry #35002ac1068bf06999abnit reliability —components/deploymentRecorder.ts— finished guard set before the write blocks the failure path's finish('failed') — filed A failed terminal write leaves a deployment row mid-phase: finish() is already latched #3501711b3ce1ee5d8f30nit reliability —bin/restart.ts— replicated restart_service on core alone throws from the replication stand-in — filed A replicated restart_service on Harper core alone ends ERROR instead of restarting this node #3508Review-Coverage: authored=claude; ran=gemini,codex,cursor-composer; adjudicated=domain; blocked=cursor-grok(no-receipt),cursor-muse(no-receipt); declined=cursor-kimi,muse; rounds=10; full=4 @ 11f0a2b
Review-Attention: deep ~60m (critical: operationDescriptions.ts, operations.ts +7; sensitive: tokenExchange.ts, credentialProvenance.ts +2; decisions: rollout-row-lock-fallback, final-write-fails-job, rollout-wait-default-on, partial-status, cleanup-owner-identity, restarted-at-naming, do-less-alternative; raised: degraded review) @ 11f0a2b