You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(gateway): warn when a worker joins with a lower lease.maxTtlMs
ADR 0005 §15 tells the operator to keep a gateway's lease.maxTtlMs at or
below every worker's, because a fleet lease's width is decided at the
gateway (admission) but dispatched as an ordinary lease.request a
lower-capped worker still refuses. Nothing enforced or surfaced that
mismatch: an operator who got it wrong only saw gateway-accepted requests
fail on some machines and not others, with the cause sitting unreported at
the exact point (a worker's view) where it was already visible.
The user decided: warn, don't clamp. Clamping the gateway's cap to the
minimum of its workers' would contradict "a fleet lease's width is decided
at the gateway" and make fleet policy drift as machines connect and
disconnect.
What changed:
- workerViewSchema gains an optional `lease: { maxTtlMs }` (schemas.ts),
projected in WorkerLink#rebuildView alongside the existing
`downloads.policy` -- the same already-fetched config.get payload, no new
round trip.
- WorkerRegistry#refresh compares an incoming worker's lease.maxTtlMs
against the gateway's own (threaded in as `leaseMaxTtlMs`, from
GatewayService down to config.lease.maxTtlMs in daemon/main.ts) and logs
a warning naming the worker (id + label), both values, and what it means.
No bus event: this is an operator configuration warning, not a business
fact about the fleet (docs/agent-rules/events.md), matching the
precedent in core/config.ts's worker-only-key warning.
Where the transition is detected, and why: inside WorkerRegistry#refresh,
by comparing the incoming lease.maxTtlMs against the *previous* view's own
value rather than tracking separate state. config.get (and so
lease.maxTtlMs) is re-read on every periodic backstop tick alongside the
catalog, not only at connect (GatewayService#runTick calls
link.refresh({ includeCatalog: true }) for every link) -- so "only warn
once per connect" would have been wrong. Comparing against the previous
value gets the right behavior for free: a worker's first refresh after
connecting has no previous lease.maxTtlMs to match, so it warns the moment
a low cap is first reported; an unchanged later refresh finds the same
value already recorded and says nothing new; a cap that drops further
warns again, since that is itself a new fact.
Tests (worker-registry.test.ts, service.test.ts at both the registry-unit
and full GatewayService-integration levels): a worker below the gateway's
cap warns; one at or above it does not; one reporting no cap at all
(config === undefined, the same condition that already leaves
downloads.policy unset) neither warns nor throws; an unchanged refresh
does not repeat the warning; a cap dropping further while already below
warns again. Reverted the registry/service/schema/link changes and
re-ran: all six new tests fail with named assertions (`expected [] to
have a length of 1`, `expected undefined to be defined`), never a bare
timeout.
Docs updated as part of this same change (the ADR is the specification
here): ADR 0005 §15 gets the warn-not-clamp sentence, and §7's "exactly
one field" becomes "two fields" now that lease.maxTtlMs travels the same
path as downloads.policy; docs/CONFIGURATION.md's lease.maxTtlMs section
gets a paragraph on the new warning. docs/adr/0005-gateway-and-worker-modes.md
has not been merged to main -- amending it here revises an unmerged record
in place, not an accepted decision. No known-pitfalls.md entry: this is a
decided, warn-only design (not an accepted gap with a planned fix), so
there is nothing pending to record there.
pnpm check is green (typecheck, typecheck:e2e, lint, format:check, unit
tests, e2e tests). Protocol range unchanged ({ min: 5, max: 5 }) -- this
is an additive schema field, no wire renegotiation involved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MA98m7ua7qvDFZjxFaww6Z
Copy file name to clipboardExpand all lines: src/gateway/test-support.ts
+14-2Lines changed: 14 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -80,6 +80,15 @@ export class ScriptedWorkerClient {
80
80
catalog: CatalogOutput=catalogFixture([]);
81
81
/** What `config.get` reports; the view carries it as a routing input (ADR 0005 §13). */
82
82
downloadPolicy: DownloadPolicy="on-request";
83
+
/** What `config.get` reports for `lease.maxTtlMs` (ADR 0005 §15) -- the routing-adjacent
84
+
* counterpart to `downloadPolicy` above. Defaults comfortably above every gateway cap this
85
+
* suite's fixtures use, so a test that never sets it cannot accidentally trip the new warning;
86
+
* a test exercising §15 sets it explicitly. `config.get` always answers with a real
87
+
* `lease.maxTtlMs` when it answers at all (the field predates this change and is not
88
+
* optional on `Config`), so unlike `downloadPolicy` there is no "unset" state to script here
89
+
* -- the workerViewSchema field's own optionality is `config === undefined`, exercised at
90
+
* `WorkerRegistry`'s own level in `worker-registry.test.ts`, not through this fake. */
91
+
leaseMaxTtlMs=24*60*60_000;
83
92
readonlycalls: string[]=[];
84
93
/** Set to reject every call with this error -- e.g. a protocol mismatch. */
85
94
failWith: unknown;
@@ -152,11 +161,14 @@ export class ScriptedWorkerClient {
152
161
}
153
162
154
163
// fallow-ignore-next-line unused-class-member -- reached structurally through the `SimlockAdminClient` the cast in `asClient()` produces; the audit cannot follow a member access through that.
// fallow-ignore-next-line unused-class-member -- reached structurally through the `SimlockAdminClient` the cast in `asClient()` produces; the audit cannot follow a member access through that.
0 commit comments