--- name: headless-session-ops description: > Use ONLY when an agent must autonomously launch a headless main session — create a session and drive agent generation over the HTTP `prompt_async` endpoint with NO persistent client attached (Web UI / TUI absent) — OR lifecycle-manage such a session afterwards: health-check polling, hung-stream diagnosis (state=generating + frozen message count), abort + re-wake rescue, provider quota-wall recovery (worker killed mid-task by a usage limit — detect, re-wake on a known-good provider, inject facts), and fact-baseline injection to correct a stale worldview in a woken worker. This is the path used by in-session agents spawning sibling sessions, `octopus run --attach` non-interactive mode, and cron / CI drivers. Covers the 3-step flow (create session → POST prompt_async WITH a known-good `model` → poll `/session/:id/message` to verify generation), how to REUSE the current session's model for the new session, the model-field requirement, the directory default (current session's directory unless targeting another repo), and the ticket-driven recipe (launch a session seeded from issue #N). Do NOT use when a persistent client (Web UI / TUI) is driving the session — those use the synchronous `prompt` endpoint and always send `model`. triggers: # English — the brand phrases for headless launch. - headless session - launch session - prompt_async - prompt async - autonomous session # English — the failure mode (agent sees a stalled session and needs to know why). - session not generating - session generation stuck # English — ticket-driven launch. - launch session for issue - launch session for ticket - spawn session for ticket # English — patrol mode (delayed self-wake polling loops, [org-internal #3937]). - patrol loop - patrol mode - delayed wake # Chinese — bare noun phrases (matches() is a contiguous substring). - 拉起主会话 - 拉起会话 - 自治会话 - 无头会话 # Chinese — patrol mode ([org-internal #3937]). - 巡检模式 - 延迟唤醒 - 会话不生成 - 会话卡死 - 为工单拉起会话 - 工单拉起会话 # English — lifecycle (hung rescue, stale-worldview correction, [org-internal #2459]). - abort session - revive session - session rescue - fact baseline # English — quota-wall recovery ([org-internal #3669]): worker killed by provider usage limit. - quota wall - usage limit reached - quota exhausted - switch provider # Chinese — lifecycle ops ([org-internal #2459]). - 唤醒会话 - 会话挂死 - 事实基线 # Chinese — quota-wall recovery ([org-internal #3669]). - 配额墙 - 配额耗尽 - 用量上限 - 换供应商 role: Producer --- > Core 中立版(Increment 6a 改写,原 deferHard verbatimDir)。机制、结构与 frontmatter 保持;实例术语(工具名、路径、工单号)按 `core/adapters/TERMINOLOGY.md` 绑定到具体实例。 # Headless Main Session Launch (prompt_async) Launch a **main session for a specific issue / work item from a headless context** — no Web UI, no TUI, just HTTP. This is the "fire-and-forget" path: an agent inside another session, a cron job, a CI script, or `octopus run --attach` non-interactive mode all land here when they need to spin up a session that actually generates. ## Directory convention — independent session in the current workspace, no worktree The default is an **independent session sharing the current session's directory** — the repo you are already working in. No new worktree, no checkout, no extra `bun install` or `.codegraph/` rebuild. Session creation and worktree discipline are **orthogonal**: this skill only opens a session record pointed at a directory; whether the launched session later creates its own worktree is a _workflow_ decision (per the worktree-discipline rule) it makes when it starts actual work — not something the launcher does. One exception: the **ticket-driven recipe** below, where claim-first ([org-internal #2297]) requires the launcher to push the workflow branch before launch, making a pre-built worktree the claim carrier + ready workspace (see Pitfall #10 carve-out). So, unless a different repo / path is specifically targeted, `?directory=` defaults to the current session's directory and is NOT mandatory: - **You are an in-session agent** (most common): set `DIR` to your own session's `directory` (you already know it from context, or read `.directory` from your own session record via `GET /session/$LAUNCHER_SID`). The new session runs independently in that same workspace. Pass an explicit different `?directory=` only when the new session must work in another **repo** — never a worktree of the same repo. The Web UI sidebar groups sessions by directory (`sidebar-project.tsx` per-workspace `workspaceSessions(directory)`, fetched via `session.list({ directory })`), so a session pointed at a worktree disappears from the workspace the creator manages. A pre-built ticket worktree is handed to the launched session via the claim comment + seed prompt, NOT via `?directory=`. - **No current session** (cron / CI / bare script): the server-side fallback when `?directory=` is omitted is the **server process's `process.cwd()`** (`server/routes/instance/httpapi/middleware/workspace-routing.ts:76`), which is wherever the server was started — unreliable. In that context `?directory=` remains effectively required. - **Dispatch convention — directory = code location, not tracker repo.** When launching a session for a ticket, set `?directory=` to the **main workspace** checkout that holds the **code the ticket edits**, which may differ from the repo where the ticket is tracked. Example: `/#` is filed in the ticket-tracker repo but edits the main repo's workflow files → the session's `?directory=` is the **code checkout root** (`//`), not the tracker repo checkout. Never point `?directory=` at a ticket worktree — the worktree goes in the claim comment + seed prompt, not the directory field (sidebar visibility, see bullet 1). The owner-check (`GET /session?directory=$DIR`) is scoped per directory, so rooting the session at the correct code checkout is also what makes the duplicate-session guard meaningful. Server-side backstop ([org-internal #3190]): creating a session whose directory IS a linked git worktree answers with an `X-Session-Directory-Warning` response header and a server warn log naming the owning main repo — if you see that header, the session will be invisible to `GET /session?directory=` owner-checks; re-check your launch parameters before proceeding. > **Authoritative reference:** `rules/headless-session-ops` wiki page > (</Octopus/octopus/wiki/rules%2Fheadless-session-ops>). > This skill is its runtime carrier. Provenance: issue **[org-internal #1695]** (which > corrected the misdiagnosed [org-internal #1691], see comment 9205). Code citations below were > verified against `` at HEAD. ## The `model` field — always provide a known-good model (read this first) `prompt_async` is **fire-and-forget**: after HTTP returns 204 (or 202 when the message is queued behind a wedge/zombie session — no live runner in that process), the server runs the full agent loop in a separate fiber. **Always include an explicit `model` in the payload, and reuse the current session's model for it** (see the next section). Two reasons: 1. **Current code does not hard-require `model`.** The HTTP boundary marks `model` as optional (`/src/session/prompt.ts:1860`), and `createUserMessage` fills a missing model via a fallback chain (`prompt.ts:858`): ```ts const model = input.model ?? ag.model ?? yield * currentModel(input.sessionID) // ^^^^^^^^^ agent ^^^^^^^^^^^^^^^^^ session/default ``` So omission does **not** produce a clean validation error — it silently resolves to the agent's model, the session's last-used model, or the provider default (`currentModel`, `prompt.ts:824-838`). 2. **But the fallback is not safe in the autonomous context.** If it resolves to a model that is unusable here (region-blocked, wrong subscription tier, not configured), the runLoop's `getModel` raises `ModelNotFoundError` (`prompt.ts:1399`, `:804-822`). In the **async** handler that error is caught by the delivery fork's failure handling (`forkPromptDelivery` in `handlers/session.ts`) and never reaches the HTTP caller — so the session looks healthy (HTTP 204, user message persisted) yet **never generates**. This is exactly the "silent stall" observed empirically in [org-internal #1695]: a session with a 1-message transcript (user only) and frozen `time.updated`. | payload | result | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `{agent, parts}` — no `model` | 204/202; model filled by fallback; if the resolved model is unusable → **apparent silent stall** (no error surfaced, no generation). | | `{agent, model:{providerID,modelID}, parts}` | 204/202; deterministic generation within seconds (verified: "1+1 等于 2。 in ~6s). | > **Iron Law:** every `prompt_async` payload MUST carry a **known-good** `model`, > and the safest known-good model is **the one driving this session right now** > (it is, by definition, generating). Never rely on the fallback chain in a > headless/autonomous launch. **Field-name gotcha when copying a model:** the prompt payload's `ModelRef` uses `{ providerID, modelID }` (`prompt.ts:1852-1855`), but the **session record's** model uses `{ id, providerID, variant }` (`session.ts:256-260`) — i.e. `modelID` is called `id`. When you reuse a model read from a session record, remap `model.id → modelID` (see recipe below). User-message `info.model` already uses `modelID` (`message.ts:403-407`). ## Reuse the current session's model (recommended default) The model powering the launching session is proven to work — reuse it. Discover it by whichever path fits your context: - **You are an in-session agent** (most common): read it straight from your own system prompt, which states verbatim: > "You are powered by the model named **{modelID}** (provider: **{providerID}**)." e.g. `modelID = "glm-5.2"`, `providerID = "zai-coding-plan"`(曾名 zhipuai-coding-plan,2026-08 更名) - **Programmatic / no system prompt** (cron, CI, sibling caller): `GET` the launching session's record and read `.model`: ```sh curl -s "$BASE/session/$LAUNCHER_SID?directory=$DIR" \ | jq '.model' # → { "id": "glm-5.2", "providerID": "zai-coding-plan", "variant": ... } ``` Then **remap** `model.id → modelID` when building the payload. - **Verify the provider is actually connected** before launching (cheap insurance — the model only generates if its provider is live): ```sh curl -s "$BASE/provider?directory=$DIR" | jq '.connected | index("")' # → null = provider NOT connected (do NOT launch); 0..n = connected ``` or call the `list-models` tool. Note: model-level availability is only knowable at runtime — a _configured_ model can still fail (subscription-tier or region errors); `GET /config/providers` lists _configured_ models, not usable ones. The safest pre-flight is "provider connected" + reusing the current session's model, which is generating by definition. There is **no `GET /model` HTTP endpoint** (it 404s); do not use it. ## Prerequisites - A reachable octopus HTTP instance (e.g. `http://127.0.0.1:4096`). - The target directory (passed as `?directory=`). Defaults to the current session's directory for in-session launches — see the Directory convention above; only specify a different one explicitly when targeting another repo. - A known-good `model` (reuse the current session's — see above). Model IDs must match the provider config in `/octopus/octopus.jsonc`; credentials live in the octopus account keyring (not env), so an isolated HOME cannot reuse them. ## The 3-step flow Set the base URL and directory once: ```sh BASE=http://127.0.0.1:4096 DIR=/data/octopus ``` ### Step 1 — Create the session ```sh SID=$(curl -s -X POST "$BASE/session?directory=$DIR" \ -H 'content-type: application/json' \ -d '{"title":"#N — ","agent":"builder"}' | jq -r .id) echo "session=$SID" ``` - `POST /session` returns `Session.Info` with `.id` (`groups/session.ts:207-211`). - `?directory=` points at the repo root. For in-session launches it defaults to the **current session's directory** (Directory convention above) — keep it that way so the new session stays visible in the creator's sidebar; pass an explicit path only when targeting a different repo (a worktree of the same repo is NOT a directory target — hand it over via the seed prompt), and always pass it from cron / CI where no current session exists (server falls back to its own `process.cwd()` otherwise). - `agent` is `builder` (the main agent). Sub-agents (explorer/worker) are spawned within a session — never named here. ### Step 2 — Deliver the seed prompt (CRITICAL: include a known-good `model`) ```sh curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ -H 'content-type: application/json' \ -d '{ "agent": "builder", "model": { "providerID": "", "modelID": "" }, "parts": [ { "type": "text", "text": "" } ] }' ``` - Endpoint is `POST /session/:sessionID/prompt_async`; it answers **204, no body** when processing starts now (idle session, live runner, noReply injection, or waiting-question preemption), **202** when the message is queued behind a session that is generating with no live runner in this process (wedge/zombie shape — stored and re-driven on restart drain or the next runner), **202** when `delay_sec` defers delivery to an in-memory delayed wake ([org-internal #3937] A — see "巡检模式 / Patrol mode" below), and **409** when the session is paused (message NOT stored) or the maintenance-mode hold queue is full. - **Global route alternative ([org-internal #4307]):** when the client only knows the sessionID (no `?directory=` routing), `POST /prompt_async` (no path prefix) resolves the instance from the app database by the body's `sessionID` and delegates to the same delivery core — same 204/202/409 semantics, same `delay_sec` contract, body is the session-scoped payload plus a required `sessionID` field. Unknown sessionID → 404 naming the session-scoped route. This is the natural fit for the completion-report POST (fixed port, no directory context). - `delay_sec` (optional, integer 0..86400, else 400) schedules the prompt for a one-shot delayed self-wake: the server holds the prompt in memory and fires it exactly once after `delay_sec` seconds, only for an idle session at intake. **Precedence ([org-internal #3937] review):** `delay_sec` applies only when the intake would otherwise start processing now (immediate class). For the queued-behind-wedge 202 member and under maintenance queue mode it is stripped — retention semantics win (the server logs the drop; the delay is NOT honored). Pending wakes are **in-memory only — a server restart abandons them**; a patrol loop must therefore tolerate a missed wake and re-arm. Never use `sleep ; curl ...` in a bash tool call to wait — that blocks the turn with zero output (the [org-internal #3937] incident shape); the bash tool now warns on bare sleeps > 120s (long-sleep guard, [org-internal #3937] C). - `model` is `{ providerID, modelID }` or the shorthand string `"providerID/modelID"` (split on the FIRST `/`; accepted since [org-internal #4307] — e.g. `"zai-coding-plan/glm-5.2"`). **Do not omit it.** Fill the values from your own session's model (see "Reuse the current session's model" above) — never hardcode a specific model. A string without a `/` is rejected with 400 naming both accepted forms. - When the seed prompt contains newlines/quotes, build the payload from a file to avoid shell-escaping errors: ```sh jq -n --rawfile p seed.txt '{agent:"builder",model:{providerID:"",modelID:""},parts:[{type:"text",text:$p}]}' ``` ### Step 3 — Verify the session actually generated (do NOT assume success) HTTP 204/202 ≠generation success. Poll until an `assistant` message appears. The endpoint is **`/session/:id/message` — SINGULAR**, and it returns a bare array whose elements are `{ info, parts }` with `role` at `.info.role` (`groups/session.ts:88,183-194`). Poll the **tail with `limit=1`** — the no-`limit` form loads the ENTIRE transcript server-side (O(messages×parts) per poll; pitfall #14's 478-message session paid it on every health check), while `limit=1` returns just the newest message (REQ-F-010 windowing, `message.ts` `page()`): ```sh # wait 5–10s, then poll the TAIL — never the full transcript curl -s "$BASE/session/$SID/message?directory=$DIR&limit=1" \ | jq '{latest_role: .[0].info.role, latest_id: .[0].info.id}' ``` Verdict: `latest_role == "assistant"` → the first generation completed (the seed is `user`; the newest message flips to `assistant` only once the model answered — an errored turn also persists an assistant message carrying the ⚠️ usage-limit part, [org-internal #2912], so check the tail text before declaring success). Stays `user` across 2–3 polls spaced 10–30 s → **apparent silent stall**; almost certainly the model did not resolve to a usable one (fallback landed badly, or you omitted `model`). Redeliver Step 2 with an explicit known-good `model`. Need everything newer than a known point? Page forward with `after=&limit=N` and follow the `X-Next-Cursor` response header — just never omit `limit`. > ⚠️ Do NOT write `/messages` (plural) — that path does not exist for listing. > (`POST /session/:id/message`, same singular path but POST, is the synchronous > prompt endpoint; don't confuse the two.) ## Launch a session for a specific ticket (#N) The common case: an agent (or cron/CI) spins up a fresh main session to work a tracked issue — seed prompt composed **from the issue body**, launched on the current session's model. **Step-0 owner check is MANDATORY ([org-internal #1803])**: four data sources (session-title scan, assignee/claim, open PRs, remote branches); any live claim → ABORT the launch (`session-scope-guard.md` points here for that pre-step). Full recipe — owner-check scan script with `X-Total-Count` / `X-Has-More` pagination handling ([org-internal #3190]), claim-first atomic 3-step ([org-internal #2297], + one-command provisioning via `script/claim-provision.sh`, [org-internal #3642]), seed-prompt MUSTs, backend duplicate-ticket hard guard ([org-internal #1989], on by default [org-internal #2350]), launch + poll commands: `reference/ticket-recipe.md` (read BEFORE creating any ticket-driven session). ## Completion-report protocol (完成回报协议, [org-internal #2374]) `prompt_async` is fire-and-forget **for the launcher too**: without this protocol the orchestrator has NO push channel and must poll forever ([org-internal #2366]). The worker actively reports `status=done|blocked|handoff` back into the orchestrator's session via `prompt_async`, with an issue-comment fallback. Default report format is `branch=`-based — workers never open PRs (TD-678/[org-internal #4425]); `pr=#N` appears only on `uncoordinated` self-opens. Hard constraints: a headless worker MUST NOT call the `question` tool ([org-internal #2378] — directional/irreversible decisions are `status=blocked` reports, then STOP), and MUST NOT self-merge its PR / self-close the issue / touch `main` ([org-internal #2386] — keep the PR open; the orchestrator merges and closes). Full protocol — orchestrator-side seed block (verbatim template), worker-side steps, decision-authority layering: `reference/completion-report.md` (read BEFORE composing a seed prompt for any worker you need to hear back from). ## Post-launch lifecycle: health check, hung rescue, stale-worldview correction ([org-internal #2459]) Launching is half the job — a headless worker can hang silently or wake with a stale worldview. Health check = two polls 30–60 s apart (`.state` + `progress` + message count); `state=generating` with ALL counters frozen across BOTH samples = hung → abort FIRST (`POST /session/:id/abort`), then re-wake with a known-good model + the anti-re-hang clause. Stale worldview → fact-baseline injection (only the CURRENT authority injects; if YOU might be the stale one, verify identity against the durable record first). Full runbooks — verdict table + [org-internal #3215] dual-sample rule, abort + re-wake sequence, fact-baseline structure, authorization asymmetry: `reference/lifecycle-ops.md` (read when managing a launched session). Quota wall — the worker's turn killed by a terminal usage-limit error (looks like silent completion: no report, ticket stalled) — has its own runbook: detection signals (finish=error ⚠️ tail, provider quota markers), provider-switch re-wake, fact-baseline template, pre-dispatch provider check: `reference/quota-wall-recovery.md` ([org-internal #3669], evidence [org-internal #3627]; read when a worker goes silent mid-task). ## 巡检模式 / Patrol mode (delayed self-wake, [org-internal #3937]) A patrol loop periodically nudges a headless session without holding a connection or burning a turn on a blocking sleep. Since [org-internal #3937] A the server supports this natively via `delay_sec` on `prompt_async`. ```sh curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ -H 'content-type: application/json' \ -d '{ "agent": "builder", "model": { "providerID": "", "modelID": "" }, "parts": [ { "type": "text", "text": "" } ], "delay_sec": 3600 }' ``` Rules of the road: - **202 + `delay_sec` ≠queued-behind-wedge 202.** Both are 202, but the delayed wake holds the prompt in memory and delivers it exactly once when the delay elapses (session must have been idle at intake; `0` equals omission → immediate 204 path). If the session was NOT idle at intake, the intake keeps the queued semantics and `delay_sec` is ignored (logged server-side) — the same precedence as maintenance queue mode, where the intake is retained and `delay_sec` never delays the flush. - **Paused at wake → dropped, not stored.** The wake's `prompt()` rejects and the server publishes an error event; the message is never stored. A patrol driver should check session state before assuming delivery. - **Restart abandons pending wakes.** They are in-memory server fibers — no persistence, no drain. After any server restart the patrol driver owns re-arming the next wake; treat a missed wake as expected, not as an error. - **Latest wake wins, per session ([org-internal #4069]).** Arming `delay_sec` on a session that already has a pending wake SUPERSEDES it: the previous countdown is cancelled, only the latest wake fires. Re-arming each turn is the intended patrol pattern — a superseded wake never delivers its prompt and publishes no error. (Pre-[org-internal #4069] stacks both FIFO — on old binaries, re-arm only after the previous wake fired.) - **Never emulate a delay with the bash tool.** `sleep 900; curl ...` blocks the turn with zero output and gets manually aborted as a hang (the [org-internal #3937] incident). The bash tool now prepends a `` on bare sleeps beyond 120s ([org-internal #3937] C) — treat that warning as a redirect to this section. ## Do NOT use this skill when - A **persistent client** (Web UI / interactive TUI) is attached — those use the synchronous `prompt` endpoint (it blocks until generation completes) and send `model` automatically. Using `prompt_async` there gains nothing and loses the in-line response. This includes the rescue runbook: a hung session driven by a persistent client is the client user's to handle (refresh / re-prompt from the UI) — abort + re-wake targets headless / managed sessions only. - You want the response **inline** in the same HTTP call — `prompt_async` is fire-and-forget; use the synchronous `prompt` endpoint instead. ## Common pitfalls | # | Pitfall | Fix | | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 1 | Omitting `model` / relying on the fallback → apparent silent stall (most common) | Always send a known-good `model:{providerID,modelID}` — reuse the current session's | | 2 | Listing endpoint written `/messages` (plural) → 404 | Use `/session/:id/message` (singular); returns a bare array, role at `.info.role` | | 3 | Copying the session record's model verbatim into the payload | Remap field names: session `.model.id` → payload `modelID` (session uses `id`, payload uses `modelID`) | | 4 | Stale `octopus` binary rejects newer config schema (`Unrecognized key`) | Use the source CLI `bun run ./src/index.ts ...`; the config itself is valid | | 5 | Missing `?directory=` query param | In-session launch: default to the current session's directory. Cron/CI (no current session): pass the repo root explicitly — server falls back to its own `process.cwd()` | | 6 | Using synchronous `prompt` for headless launch | `prompt` blocks until done — wrong for fire-and-forget; use `prompt_async` | | 7 | Shell-escaping errors in a multi-line seed prompt | Build the payload with `jq -n --rawfile` from a file | | 8 | Verifying models via `GET /model` — no such endpoint (404) | Use `GET /provider` → `.connected` (provider live check); `list-models` tool also works. `GET /config/providers` lists _configured_ models only | | 9 | Launching a session for a ticket another live session already owns → conflicting diffs, duplicate work ([org-internal #1744]/[org-internal #1753]) | Run the Step-0 owner check (`GET /session` title scan + worktree/branch/PR scan); ABORT and report on conflict | | 10 | Creating a worktree / fresh checkout for the new session (unnecessary `bun install` + `.codegraph/` rebuild cost) | Don't — for _general_ launches: a session is a conversation pointed at a directory, so reuse the current workspace; worktree creation is a separate _workflow_ decision the launched session makes if/when it starts a workflow. **Ticket-recipe carve-out ([org-internal #2297]):** the ticket's work happens in a dedicated worktree anyway (worktree discipline), and claim-first pushes the workflow branch before launch — so the launcher pre-builds the worktree as the claim carrier + ready workspace, and the session reuses it (cost is front-loaded, not wasted). The worktree path goes in the claim comment + seed prompt; `?directory=` stays on the creator's workspace | | 11 | Launching a worker and never hearing back — orchestrator polls a few times, stops, loses visibility ([org-internal #2374]) | Append the completion-report block to the seed prompt (Completion-report protocol section): worker `prompt_async`s a one-line status back into the orchestrator session on done/blocked/handoff, with issue-comment fallback | | 12 | Headless worker calls the `question` tool → no attached client: the question deadlocks in a queue, or `auto_approve` silently auto-approves a direction-setting decision ([org-internal #2378]) | NEVER call `question` from a headless worker. Decision boundary (Completion-report protocol section): directional/irreversible decision → `status=blocked` report + one-line decision point, then STOP; the orchestrator is the sole human-decision entry point | | 13 | Headless worker self-merges its PR / self-closes the issue once the PR looks ready, despite a seed instruction to keep it open ([org-internal #2386]; N-02 [org-internal #2367] / N-03 [org-internal #2368], commits `d1565c99` / `040f21b7`) | Self-merge / self-close / touching `main` are irreversible directional actions → blocked by the Decision boundary. PR ready → keep it OPEN, report `status=done branch=`; the orchestrator opens/admits the PR, merges and closes uniformly | | 14 | Trusting `state=generating` as "alive" — a hung stream goes unrescued (session A, 2026-08-16: `generating` 6+ min, count frozen at 478) | Poll state AND message count twice 30–60 s apart; frozen count = hung → abort + re-wake (Post-launch lifecycle section) | | 15 | Re-sending a wake prompt to a hung session and waiting — queued prompts are never consumed while the stream is dead | Abort FIRST (`POST /session/:id/abort` → 200, verify `idle`), THEN re-send; a prompt queued before the abort may still never fire — always re-deliver | | 16 | A worker woken from restart / long idle acts on its stale worldview — re-dispatches superseded work or claims authority it no longer holds (2026-08-16: revived worker re-ran an already-published FAIL verify; ops session announced an inherited "orchestrator" takeover) | Fact-baseline injection (Post-launch lifecycle section): authoritative facts + invalidated assumptions + one concrete re-assignment. Only the CURRENT authority injects; a session that suspects IT is stale verifies identity against the durable record first (`rules/compact.md` identity clause) | | 17 | Worker goes silent mid-task: provider quota wall killed the turn (usage limit / 429) — no completion-report, no blocked report; from the issue side indistinguishable from silent completion (2026-08-29 [org-internal #3627]: opencode-go `weekly usage limit reached` after the 13:18 Round-1 FAIL; ticket stalled until manually re-woken) | Detect via tail probe (`finish=error` + ⚠️ usage-limit part, `metadata.reason` in the quota family) + `GET /provider` quota markers; re-wake on a known-good provider with the fact-baseline template — `reference/quota-wall-recovery.md` ([org-internal #3669]). Prevent: pre-dispatch provider check + the seed's 配额自报 clause (`reference/completion-report.md`, [org-internal #3669]) — quota exhaustion is a `status=blocked reason=quota-exhausted` report, not a silent death | ## Known limitation (separate from this workaround) A missing `model` producing an **apparent silent stall** (no surfaced error, no generation) is an observability / robustness gap. Current code does NOT hard-require `model` — it fills it via a fallback chain (`prompt.ts:858`) and any resolution failure surfaces as a published `Session.Event.Error` on the synchronous path (`prompt.ts:804-822`), but the **async** `prompt_async` handler catches that cause (`forkPromptDelivery` in `handlers/session.ts`) so the HTTP caller never sees it. The proper fix is to surface the error (or reject unusable models at the boundary) so a stalled session is diagnosable. Until then, **always provide an explicit known-good `model`** — this skill is the documented workaround (see [org-internal #1695] "遗留 minor"). **Update ([org-internal #2912], 2026-08-20)**: usage-limit turn failures (HTTP 429 / FreeUsageLimitError / GoUsageLimitError) are no longer silent shells. When such a turn terminates (retry schedule ends or the spin is aborted), the errored assistant message persists `finish=error` plus a human-readable ⚠️ text part carrying provider, limit reason, and reset hint (`metadata: { error: true, reason, provider }`) — visible via `GET /session/:id/message` and rendered in the web UI like any assistant text. Pitfall #14's double-poll is still the liveness check while a 429 retry spin is IN progress (the spin itself retries with backoff until interrupted); the explicit error part only appears once the turn ends. The general async-path observability gap above remains tracked by [org-internal #1695]. ## See also - Source issue / full manual: **[org-internal #1695]** (this skill adds the ticket-driven recipe - current-model reuse, and corrects the endpoint/claim against current code). - Completion-report protocol: **[org-internal #2374]** (worker-side active report back to the launching orchestrator session; fallback to issue comment). - Quota-wall recovery recipe: **[org-internal #3669]** (evidence instance [org-internal #3627]) — detect a worker killed by a provider usage limit, re-wake on a known-good provider, pre-dispatch prevention. - Decision boundary: **[org-internal #2378]** (headless worker MUST NOT call `question`; directional decisions go through `status=blocked` reports to the orchestrator) and **[org-internal #2386]** (the worker MUST NOT self-merge its PR, self-close the issue, or touch `main` — the orchestrator merges and closes uniformly). - Correction of the prior misdiagnosis: **[org-internal #1691]** (closed Invalid), comment 9205. - Synchronous client always sends `model`: `packages/app/src/components/prompt-input/submit.ts:158-165`. - Code citations: `/src/session/prompt.ts:858,804-822,1399`, `handlers/session.ts` `forkPromptDelivery` (async failure handling), `server/routes/instance/httpapi/groups/session.ts`. ## References **On-demand references** (NOT injected — read at the declared timing): - `reference/ticket-recipe.md` — ticket-driven launch (`Launch a session for a specific ticket (#N)`): read BEFORE creating any session for issue #N — Step-0 owner check, seed-prompt composition, launch + poll commands. - `reference/completion-report.md` — completion-report protocol (完成回报协议, [org-internal #2374]): read BEFORE composing a seed prompt for a worker you need to hear back from — orchestrator block template, worker-side report steps, decision boundary ([org-internal #2378] / [org-internal #2386]), quota self-report clause ([org-internal #3669]). - `reference/lifecycle-ops.md` — post-launch lifecycle ([org-internal #2459]): read when managing a launched session — health-check polling, hung-stream diagnosis (state=generating + frozen counters), abort + re-wake rescue, fact-baseline injection, authorization asymmetry. - `reference/quota-wall-recovery.md` — quota-wall recovery ([org-internal #3669], evidence [org-internal #3627]): read when a worker goes silent mid-task (suspected provider usage limit) or BEFORE dispatching a long-running worker — detection signals (finish=error ⚠️ tail, provider quota markers, log grep patterns), provider-switch re-wake + fact-baseline template, pre-dispatch provider check, quota self-report clause. - `rules/headless-session-ops` wiki page — authoritative reference (this skill is its runtime carrier; link in the Directory-convention note above). - `core/rules/session-scope-guard.md` — owner-check + claim-first rule (the ticket recipe's Step 0 is its launch-time application). - `core/rules/compact.md` — identity-verification clause backing the authorization-asymmetry rule (`reference/lifecycle-ops.md`).