Primary-action instruction selection¶
Boundary and ownership¶
This Stage 2B slice removes the Python instruction decision owner while keeping
caller-visible prose and rendering stable. projectInteractionExecution in
work_items/interaction_contract.ts selects the existing mode and its primary
instruction in the same work_item.interaction_execution.project request.
There is no new operation, handler registration, runtime, provider or store.
Qualification remains incomplete until the validation holds below are resolved.
Retired Python symbols in work_items/primary_action.py:
protocol_first_candidate_action: source precedence, addressed labels, capability candidate count, scoped successor and advancement fallbacks.protocol_replan_action_packet_instruction: settlement-only, successor and generic typed-outcome wording.resolve_canonical_primary_action: exact mode prose, fallbacks and child/peer lane counts, including the empty-child-list precedence over peer lanes.
interaction_contract.py::_project_interaction_execution sends a single optional
facts/views carrier and the existing action_selection_context projection.
The final contract builder requests and threads the returned instruction through
_build_interaction_agent_channel into build_primary_action_projection.
unadmitted_action_selection retains its mode-only preflight and exact result
shape. The TS owner composes actionSelectionNeedsRecovery before reading
instruction facts; rejected/deferred recovery does not construct executable
instructions. The existing Python recovery alternative, selection/capability
channel overrides, blocked-successor override and blocked-wait override remain.
Python retains protocol_action_text and protocol_action_label: Python str
and nested repr, truthiness, whitespace, label prefixes and code-point limits
are compatibility codecs, including ... rather than the trace codec's ….
primary_action_instruction_facts transports those views and existing Todo
eligibility/lifecycle source facts; it does not select an instruction or retry a
Python fallback. Python transports at most the first selectable row per list
source, evaluated from the same coerced views; the typed owner applies the same
predicates and source precedence. Request size is independent of backlog/Todo
list length. Registered effective actions and agent-scope values come from
their generated TS exports. Other existing local rule strings keep their exact
spellings; no shared vocabulary or persisted schema is added.
Python also retains protocol_monitor_action and the resolution trace through
state_projection.next_action_resolution_trace. primary_action still precedes
optional resolution_trace; channel insertion order and final Python JSON
serialization are unchanged. Delete the trace/monitor facade only when their
remaining consumers adopt a complete typed action/trace projection. Delete the
scalar/label codecs and facts carrier only after the last Python text/label
consumer exits and a complete typed compatibility codec proves Python coercion,
Unicode, truncation and trace parity. This slice does not retire those owners.
Correction (2026-11-02): protocol_monitor_action had no remaining caller
and is removed. The resolution trace remains live.
Vocabulary budget¶
The registry's meaning says "module_budget counts modules under loopx/ whose text carries the standalone field token" and "the token count stays until Q11 decides whether the syntactic metric replaces it"; this deliberately conservative lexical proxy protects against unexplained growth in potential field readers.
On the same tracked-source workload, base be1dc37a0 → this commit moves goal_boundary lexical counts from Python/TypeScript 30/2 → 29/3 (total 32 unchanged), originally failing legacy field goal_boundary grew to 3 .ts modules; budget is 2 and retrodiction (13, 15) versus frozen (12, 15).
The moved sentence, repair goal_boundary.write_scope projection before attempting the selected write, is byte-identical agent-facing prose, adding no reader, writer or binder; the smoke's prose-versus-reader distinction keeps migration surfaces at Python 16 / TypeScript 1.
Retaining that instruction, the registry tightens the Python token budget to its measured 29 and increases TypeScript 2 → 3, with zero lexical headroom; migration-surface budgets and their anchor remain unchanged at 16/1, while the registry and RETIREMENT_ANCHOR move together in this same commit, as the anchor mechanism requires; the retrodiction baseline is unchanged and the measured result returns to (12, 15) (would_have_caught=12/15, caught_baseline=12/15) once the budget accepts the measured counts.
Economics and parity¶
Against the immutable base, the three retired function spans contain 267 physical
lines (269 including separating lines). Production Python is 185 lines smaller;
TS grows by 109; net production is 76 lines smaller. The primary-action module
shrinks 347 → 152 lines. The new Python facts codec is 77 lines plus its
separators; remaining Python changes are transport/result plumbing. The TS
helper contains 14 lines of local view decoders (facts, object, text,
rows, count), not another RPC surface. The bounded
companion refactor reuses action_selection_context rather than a second
admission-fact encoder. Broader admission, monitor and trace migration is deferred.
One untimed base/head execution and one logical-request trace per command, with isolated synthetic SQLite fixtures and one or three delegation bindings:
| Command | Bindings | Crossings base → head | Stdout bytes base → head |
|---|---|---|---|
agent-context --phase before_plan |
1 | 1 → 1 | 1885 → 1885 |
agent-context --phase after_delegate_result |
1 | 3 → 3 | 1612 → 1612 |
| Same result phase with a Turn instance | 1 | 3 → 3 | 2594 → 2594 |
quota should-run |
1 | 75 → 75 | 25520 → 25520 |
agent-context --phase before_plan |
3 | 1 → 1 | 2247 → 2247 |
agent-context --phase after_delegate_result |
3 | 3 → 3 | 1612 → 1612 |
| Same result phase with a Turn instance | 3 | 3 → 3 | 2594 → 2594 |
quota should-run |
3 | 75 → 75 | 25904 → 25904 |
All eight normalized JSON outputs match after excluding timestamp keys ending
in _at, context_id, event_id and staleness_seconds. This is equal byte
count and normalized-content evidence, not literal equality of volatile fields
or a latency improvement claim. No persisted-format change or data migration.
Validation and remaining gates¶
A pre-change synthetic fixture pins 182 cases: all modes, candidate-source
precedence, fallback and lane-count branches, empty/invalid source shapes,
booleans, numbers, nested values, whitespace, Unicode/non-BMP and truncation.
Native tests check those independently pinned instructions and the recovery
getter sentinel; Python checks codec views, non-JSON str and projection order.
Existing Python CLI/Host integration assertions are retained. Retire the pinned
Python payload/view fixture when its last codec consumer exits and native
invariant tests independently cover the same wording/source branches.
The oversized-backlog regression pins 22,000 rows through the public Python
projection: the lane instruction matches BASE, and rejected selection preserves
the BASE recovery command with must_attempt and delivery_allowed both false.
Encoded instruction facts stay below 16 KiB.
Independent review's rule table found EQUIVALENT selection semantics with one
DIVERGENT row for oversized eager transport; the bounded first-selectable-row
carrier fixes it: facts for 22,000 rows shrink 2,354,852 → 959 bytes, with lane
and rejected-selection recovery outputs byte-identical to base and pinned in
tests. The fix passes 38 arbitration tests, 37 native tests and 309 Python
adapter tests, including 182-case typed-instruction parity. The goal_boundary
vocabulary budget decision moves Python/TypeScript 30/2 → 29/3 together with
RETIREMENT_ANCHOR in this commit; the smoke reports
semantic-vocabulary-drift-smoke: ok, architecture tests pass 128/128, and
retrodiction returns to the unchanged 12/15 baseline. The full TS suite passes
5,528 tests with 0 failures and 75 PostgreSQL skips. The original full Python
suite records 27,169 passed, 908 failed, 234 skipped and 8 errors; base replay
of its 147 failing files records 2,353 passed, 905 failed, 18 skipped and
8 errors, reproducing 913/916 distinct failure/error IDs under the same guard,
whose restrictions induced some failures. Of the three head-only failures,
the two architecture failures are repaired above; the third is
environment-induced because dashboard dependencies were installed mid-validation,
while base skips that test. The final-code 44-module sweep has identical sets
of 84 failures on base and head, zero new failures, 1,871 → 1,880 passes and
20 skips on each; these scoped runs do not establish a green full Python suite.
The final canary reports status: passed, ok: true, merge_gate_passed: true,
manual_holds: 0, with 0 failures. The four interleaved benchmark runs
(25 timed runs and 3 warmups per command) retain equal bytes on all eight
commands and unchanged 1/3/3/75 crossings for both binding counts; every median
regression is within the stated 5% rule, so byte, crossing and median checks
pass without a latency-improvement claim. CI-only limits remain a real
PostgreSQL server, Node 22.22.3, Windows and browser lanes; PostgreSQL skips
are not passes. Main's inherited failures remain tracked by #7433–#7436 and
7389; this validation does not resolve them.¶
Installed wheel, minimum Node, Windows, real PostgreSQL and full CI are not qualified locally; PostgreSQL skips are not passes. The committed-head bilingual check and premerge canary remain orchestrator/validation responsibilities. These holds do not authorize broader authority/provider changes or weaker acceptance. Rollback is a code-only revert; no state rewrite is needed.