Skip to content

Enforce spawn/resume-only agent delivery: remove waitForInjected/sendInput once the last 3 call sites migrate (lint + test guards, babysitter coalesced-resume) #105

Description

@khaliqgant

Goal

Make live injection into a running agent structurally impossible, so all agent-directed delivery is spawn-or-resume only. Escalation/dispatch fragility (#94/#96/#98) all traced to injecting into a live TUI; #104 removes most of it, but nothing stops it being reintroduced (e.g. the babysitter wake in #100 still injects). A convention won't hold — this needs enforcement.

The invariant

An agent receives input only at spawn (the spawn task) or via resume (re-spawn with context). Live injection into a running session is prohibited.

The surface

Two optional methods on the FleetClient port carry all injection:

  • waitForInjected?(input, opts)src/ports/fleet.ts:66
  • sendInput?(name, data)src/ports/fleet.ts:67

resume(input) (fleet.ts:53) is the durable alternative. Call sites in factory.ts: 12 on main, 3 after #104. Small, reachable target.

Enforcement ladder (weakest → airtight)

  1. Test invariant (now, cheap). Count waitForInjected/sendInput calls in FakeFleetClient; assert the babysitter-wake path (and durable dispatch) produce zero. feat(dispatch): opt-in spawn-task delivery to avoid the codex injection race (#98) #101/Make human escalation durable through GitHub issues #104 already do a version for dispatch (expect(fleet.messages).toEqual([])). Catches regressions before the primitive is gone.
  2. CI lint guard (now). no-restricted-syntax (or a CI grep) that fails on any new .waitForInjected( / .sendInput( outside an explicit legacy-compat allowlist. Reintroducing injection becomes a deliberate, reviewer-visible allowlist edit — not an accident.
  3. Single delivery seam (structural). Route all agent-directed delivery through one method (#deliverToAgent(record, spec, content)) whose only implementations are spawn-task and resume. Delivery code never touches the fleet injection methods directly.
  4. Delete the primitive (airtight). Once the last 3 call sites migrate, remove waitForInjected/sendInput from the port and all impls. The type system then enforces it — you cannot call a method that doesn't exist. Only this is airtight; 1–3 are the bridge to it.

The design tension to resolve first (babysitter cadence)

The babysitter is why injection was tempting: it's long-lived and reacts to many small PR events (review comments, CI). Naïve "no injection" → resume-per-event → a cold-start on every wake = expensive, and someone will "temporarily" reach for injection again. So enforcement must pair with coalesced resume: batch events, re-spawn with the batch. #100's durable subscription (claim/ack) already provides the coalescing primitive — deliver the batch via resume, not sendInput. Solving the cadence is a prerequisite for step 4 being acceptable.

Sequence

  1. Land Make human escalation durable through GitHub issues #104 (removes 9 of 12 injection calls; durable issue-comment escalation).
  2. Add the test invariant (1) + lint guard (2) now, allowlisting the remaining 3 call sites.
  3. Migrate the last 3 to resume — babysitter wake via coalesced resume (pairs with feat: consume durable Relayfile babysitter subscriptions #100).
  4. Delete waitForInjected/sendInput from the port + impls; drop the allowlist.
  5. Document the invariant (ADR / CONTRIBUTING) so the guard and reviews have a rationale to cite.

Acceptance

  • No .waitForInjected( / .sendInput( call sites remain in src/ (grep-clean).
  • The methods are absent from FleetClient (src/ports/fleet.ts) and all impls — build fails if re-added.
  • Babysitter wakes are delivered via coalesced resume with no per-event cold-start regression.
  • A test asserts durable dispatch + babysitter wake perform zero live injection.

Refs: #99 (escalation redesign), #104 (removes 9/12 — the enabling step), #100 (durable babysitter subscription — its wake delivery is one of the last 3 to migrate; provides the coalescing primitive), #98/#96 (the failures live injection caused).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions