fix(kernel): refuse an executor backend nothing implements - #956
Merged
Conversation
snapshotExecutorConfig switched over the seven ExecutorConfig arms with no default. The backend is data a profile, an experiment config, or a replay journal can carry, so a name outside the union reaches the factory untyped: the switch returned undefined, createExecutor handed back a working-looking factory, and the failure landed one call later as a TypeError naming nothing. It now refuses by name, listing the supported backends read off the trace-propagation table, which is the one copy the compiler already forces to hold every arm. Three prose sites named a stale backend set and are corrected: CLAUDE.md listed five of seven, docs/architecture.md paraphrased four and cited a line number 3218 lines away from createExecutor, and the ExecutorConfig doc comment listed six. The supervisor's own header claimed a teardown failure is journaled as a `cancelled` event; it is journaled per node as `teardown-unconfirmed`.
tangletools
approved these changes
Aug 21, 2026
tangletools
left a comment
Contributor
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 3dd4e166
This PR was opened by the trusted drewstone account.
This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.
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.
Problem
snapshotExecutorConfig(src/runtime/supervise/runtime.ts:4550) switches over the sevenExecutorConfigarms and has no default. TypeScript accepts that, because the switch is exhaustive over the union — but the union is not what arrives. The type's own docstring says so: "the backend is DATA — the cost dial a profile, an experiment config, or a replay journal can name." A profile, a config file, or a journal line carries a string, not a discriminated union member.Given a name outside the union, the switch returns
undefined,createExecutorbuilds and returns a factory anyway, and the failure lands one call later:That TypeError comes from
runtime.ts:4738, names no backend, and points at a line the caller did not write. The house rule is the opposite: no fallbacks, fail loud, typed errors.I found this while checking whether
bridge-worktreeis an unowned eighth backend. It is not — it is a label onExecutorMaterialization.backend, which isstringby design because providers and BYO executors stamp their own names, and it has a producer (runtime.ts:4286), a consumer that names it by hand (stream-agent-turn.ts:319) and a passing test (tests/runtime/worktree-cli-executor.test.ts:709). The claim was wrong; the missing refusal it led me to is real, and applies to any unrecognized backend.Change
snapshotExecutorConfiggains adefaultarm that throwsValidationErrornaming the value and the supported set:The supported set is read off
WORKER_TRACE_PROPAGATION(supervise/worker-trace.ts:81) rather than written out again. That table issatisfies Record<ExecutorConfig['backend'], boolean>, so the compiler already forces it to hold every arm — it is the one copy that cannot go stale, and reusing it means this change adds no eighth list.The prose this exposed
Tracing the backend vocabulary turned up four descriptions of it, three of them stale, all corrected here:
CLAUDE.md:67router/router-tools/bridge/cli/sandbox— five of sevendocs/architecture.md:456runtime.ts:1517runtime.ts:4735ExecutorConfigdoc commentruntime.ts:4522supervisor.ts:14cancelledevent"teardown-unconfirmed, which is whatsupervisor.ts:749writesThe last one is worth its own line:
SpawnEvent.cancelledhas eight readers and, in the product, zero writers, and that sentence is why it reads as an intended-but-missing writer. It is not. A cancelled node is journaled assettledwithstatus: 'down'(scope.ts:1785) — the shapeconformance/capabilities.jsonregisters undercancellation-acknowledgementandtests/kernel/worker-cancellation.test.tspins — plus a durable control record naming the effect (coordination-driver.ts:509). Driving a real cancel through the real runtime and dumping every journal line confirms it: eleven lines, onesettled/downcarrying the reason,cancelledevents: 0. The member stays because it is the replay vocabulary for an externalSpawnJournalimplementor; deleting it produces 12 type errors in the replay and tree-view arms. The stale sentence was the whole defect.docs/architecture.md's citation was also 3218 lines away from the symbol it named. The freshness gate (scripts/check-docs-freshness.mjs) checks that a cited file exists and never that the line is right, andCLAUDE.mdis not in itscuratedDocslist at all — so all three sites were green while wrong. I am not adding a gate class for that: per the standing rule, a gate whose defect has occurred once and whose invariant a type could carry is the wrong tool, and here the type already carries it (WORKER_TRACE_PROPAGATION). Naming the blind spot is the deliverable.Proof
The new test fails before the change with
AssertionError: expected [Function] to throw an error— measured by revertingruntime.tsand re-running it.Simplification
Simplification: the backend vocabulary now has one description in prose (all seven, everywhere) and one machine-checked list (
WORKER_TRACE_PROPAGATION), which the refusal reads instead of restating; theExecutorConfigdoc comment stops carrying a list that can drift.Net: +19 / -6 lines across 4 files plus one regenerated
docs/api/runtime.md; 1 stale list deleted, 0 lists added.Not done here:
ExecutorMaterialization.backendstaysstring. It is written by providers and BYO executors with names this package cannot enumerate, and the one place it is load-bearing —assertExactExecutorDeclaration(stream-agent-turn.ts:283,:319) — has no test at all. That is a separate change with its own reproduction.Tests: +1 (
createExecutorrefuses an unimplemented backend by name — the case that today returns a working-looking factory and fails one call later as an unattributed TypeError), -0 deleted.Refs #954