# AUTOPILOT-65 — completions and ticks are computed on time; the orchestrator only "hears" them at a turn boundary

## The symptom, observed live (twice, one session)

1. An `/autopilot off` confirmation, delivered via `deliverAs: "followUp"`,
   sat undelivered while the orchestrator issued a long, continuous run of
   its OWN tool calls, surfacing only much later.
2. Four dispatched workers (AUTOPILOT-51, -53, -55, -61) completed —
   their `status.json` files record `state: complete` — but their
   completion notifications never reached the conversation during a
   ~45–60 minute stretch where the orchestrator was continuously issuing
   tool calls for an unrelated task (the AUTOPILOT-32 merge: rebase,
   conflict resolution, typecheck diffing, dotfiles merge). The orchestrator
   believed those workers were still "active" and reported the fleet as "at
   capacity" when real capacity was zero — a materially wrong operational
   picture, caught only because the user asked "are these actually
   running?"

## Hypothesis (stated up front, verified below against the actual code)

Background completion/tick delivery requires a yield point — the current
turn ending — to actually reach the orchestrating session, the same
"delivery is gated on settle" shape AUTOPILOT-51 found for the tick
*telemetry* path. A sufficiently long, uninterrupted stretch of the
orchestrator's OWN tool calls (i.e. a turn that never ends) can prevent that
yield point from occurring at all, so completions and ticks queue invisibly
instead of interrupting.

**Verified: true**, by reading the actual delivery mechanism the code uses
— not by inference from the symptom alone. This is a distinct issue from
AUTOPILOT-51 (see "Relationship to AUTOPILOT-51" below): AUTOPILOT-51 is
about a tick's *content never being written to the audit trail*; this is
about a tick or message that WAS generated correctly never being *seen* by
the orchestrator's conversation in a timely way, because of how the host
runtime queues `followUp` deliveries.

## What the code actually does — the delivery mechanism

Every host-to-orchestrator message this framework sends — ticks (dispatch/
intake/review/ship/recover/decision/...), the `/autopilot on|off`
confirmation, the `/orchestrate` procedure injection — funnels through
`pi.sendMessage` / `pi.sendUserMessage` with `deliverAs: "followUp"`
(`src/hosts/pi-extension.ts`):

```ts
// src/hosts/pi-extension.ts:150 (orchestrate injection)
const sent = pi.sendUserMessage(procedure, { deliverAs: "followUp" });

// src/hosts/pi-extension.ts:240 (tick delivery — deliverTick, the ONE
// choke point both the runner and the decision panel funnel through)
const sent = pi.sendMessage(
  { customType: TICK_TYPE, content: message, display: true },
  { triggerTurn: true, deliverAs: "followUp" }, // historical — superseded by AUTOPILOT-125: a tick no longer triggers a turn
);

// src/hosts/pi-extension.ts:315 (/autopilot on|off confirmation)
const sent = pi.sendUserMessage(msg, { deliverAs: "followUp" });
```

The comments already on this code state the contract plainly:

> `deliverAs: "followUp"` is REQUIRED — `sendUserMessage` THROWS "Agent is
> already processing..." while the agent is streaming (e.g. mid-command)
> ... `followUp` queues the injection **until the current turn finishes**.

and, for the busy-but-not-streaming case, the framework defers to the
*next settle*:

```ts
// src/hosts/pi-extension.ts:140-145
// deferAs: when the agent is busy but NOT streaming, the runtime's
// followUp is ignored and sendUserMessage THROWS — defer to the settle.
if (agentBusy) {
  runner.deferUserMessage(procedure); // shared deferral — flush at the settle
  return;
}
```

And the flush point for every deferred message and every queued harness tick
is `agent_settled`:

```ts
// src/hosts/pi-extension.ts:358-361
pi.on("agent_settled", () => {
  agentBusy = false;
  runner.onSettled(); // the runner flushes the shared deferral + harness
                       // queue at the settle (the agent is idle)
});
```

So both delivery paths — the immediate `followUp` send AND the explicit
deferred queue — resolve to the same fact: **nothing reaches the
orchestrator's conversation until the current turn ends.** `docs/queue-model.md`
already documents the consequence for tick CONTENT ("Delivery-time facts") —
a deferred tick is re-derived fresh at the point it is finally delivered, so
its numbers are never stale — but that section assumes delivery eventually
happens soon. It does not cover the case this ticket observed: a turn that
does not end for 45–60 minutes because the orchestrator keeps issuing its
own tool calls back to back, with no idle window in between.

**Worker/reviewer completions are the same choke point.** A completion event
(`subagent:async-complete`) is processed immediately and correctly —
`runner.ts`'s `onCompletion` flips the queue item (`active → ai-review`),
persists `reviewTargets`, calls `flagForReview` on a PASS, etc., all
synchronously against the store. That is why `status.json` and the queue
store were already correct (`state: complete`) when the user checked. But
the NOTIFICATION that tells the orchestrator's conversation this happened —
`sendTick(result.tick)` — goes through the exact same `deliverTick` →
`pi.sendMessage(..., { deliverAs: "followUp" })` path. The state update and
the notification are decoupled: the former is unconditional and immediate,
the latter is gated on the same turn-boundary yield point as every other
tick.

## Why this reads as "at capacity" being wrong

Fleet occupancy, as reported to the orchestrator, comes from ticks and from
`subagent({ action: "status", view: "fleet" })` calls the orchestrator makes
itself. If four workers finish during a stretch where the orchestrator never
calls that tool and never receives a completion tick (because it never
yields), its last-known picture of the fleet is stale by exactly the length
of that stretch. There is no polling or push mechanism independent of a
turn boundary that would correct this automatically — the orchestrator has
to ask.

## Is this actionable in this repo?

**Not as a code fix.** `deliverAs: "followUp"` / `triggerTurn` are pi host
runtime primitives (`pi.sendMessage`/`pi.sendUserMessage`) that this
repository calls but does not implement or control — there is no vendored
SDK type or source for them in this tree, and the framework already picks
the only delivery mode that does not throw while the agent is
processing/streaming (see the comments quoted above; the alternative,
`deliverAs` without `followUp`, throws synchronously mid-turn). The runner
already does the correct thing with what the host provides: it retries
async rejections, re-derives tick content fresh at actual delivery time
(`docs/queue-model.md`, "Delivery-time facts"), and flushes every deferred
message and every queued harness tick at the very next settle
(`onSettled`). There is no in-repo lever that forces the host runtime to
interrupt a long, uninterrupted stretch of tool calls the orchestrator LLM
itself is choosing to issue — that would require the host runtime to expose
a different delivery primitive than `followUp`, which is outside this
repo's control.

> UPDATED (KEY: AUTOPILOT-125): the tick no longer sends `triggerTurn` at all —
> the historical option in the snippet above is kept for the record, but a tick
> is now a NON-triggering custom message: core RECORDS it in the transcript and
> does NOT start a turn. The delivery-gating analysis here is unchanged (a
> `followUp` custom message still surfaces at a turn boundary); what changed is
> that the tick can no longer be the thing that CREATES that boundary. See
> `docs/AUTOPILOT-51-tick-audit-gap.md`, "The NON-TRIGGERING tick".

**What IS actionable: the queue store itself is a synchronous, always-current
source of truth**, independent of tick delivery. `queue_list` and
`subagent({ action: "status", view: "fleet" })` read the live state directly
— they do not depend on a tick having been delivered. The gap AUTOPILOT-65
exposes is procedural: the orchestrator has no standing instruction to
proactively re-check that live state during a long stretch where it is not
otherwise yielding control back to the runtime. It waits to be interrupted,
and the interrupt mechanism it is waiting on is exactly the one that cannot
fire until it stops.

## The fix

Documentation only (see `prompts/orchestrate.md`'s loop section and
`skills/orchestrator-operations/references/pi.md`): a standing rule that
after any long stretch of continuous, self-issued tool calls — a rough
heuristic (~15-20 consecutive tool calls) or any multi-step git operation
sequence (rebase / conflict resolution / merge) — the orchestrator
proactively calls `queue_list` (and `subagent status fleet` if it holds any
belief about occupied slots) to reconcile against the live store, rather
than trusting a tick or completion notification to interrupt it. This
mirrors "Re-read state" (loop step 1) but makes it happen mid-investigation,
not only when pinged — because during exactly this failure mode, no ping
arrives.

## Relationship to AUTOPILOT-51

Same family (something computed correctly is never observed by the
orchestrator) but a different cause and a different fix:

- **AUTOPILOT-51**: the tick's *content* was correct at generation, but
  most tick-delivery call sites never wrote a telemetry record at all — an
  in-repo instrumentation gap, fixed in-repo (`logTickDelivery` at the
  delivery choke point).
- **AUTOPILOT-65**: the tick/completion notification's content is correct
  AND (post-51) is now logged — but its *delivery to the live conversation*
  depends on the host runtime reaching a turn boundary, which the
  orchestrator's own uninterrupted tool-call behavior can indefinitely
  postpone. This is a host-runtime property, not an in-repo instrumentation
  gap, so it stays a separate ticket with a procedural (not code) fix.
