2026-09-15 08:41:51 +08:00
---
name : headless-session-ops
description : >
2026-09-16 13:13:18 +08:00
Use ONLY when an agent must autonomously launch a headless main session —
2026-09-15 08:41:51 +08:00
create a session and drive agent generation over the HTTP `prompt_async`
2026-09-16 13:13:18 +08:00
endpoint with NO persistent client attached (Web UI / TUI absent) — OR
2026-09-15 08:41:51 +08:00
lifecycle-manage such a session afterwards: health-check polling, hung-stream
diagnosis (state=generating + frozen message count), abort + re-wake rescue,
2026-09-16 13:13:18 +08:00
provider quota-wall recovery (worker killed mid-task by a usage limit —
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
the 3-step flow (create session → POST prompt_async WITH a known-good
`model` → poll `/session/:id/message` to verify generation), how to REUSE
2026-09-15 08:41:51 +08:00
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)
2026-09-16 13:13:18 +08:00
is driving the session — those use the synchronous `prompt` endpoint and
2026-09-15 08:41:51 +08:00
always send `model`.
triggers :
2026-09-16 13:13:18 +08:00
# English — the brand phrases for headless launch.
2026-09-15 08:41:51 +08:00
- headless session
- launch session
- prompt_async
- prompt async
- autonomous session
2026-09-16 13:13:18 +08:00
# English — the failure mode (agent sees a stalled session and needs to know why).
2026-09-15 08:41:51 +08:00
- session not generating
- session generation stuck
2026-09-16 13:13:18 +08:00
# English — ticket-driven launch.
2026-09-15 08:41:51 +08:00
- launch session for issue
- launch session for ticket
- spawn session for ticket
2026-09-16 13:13:18 +08:00
# English — patrol mode (delayed self-wake polling loops, [org-internal #3937]).
2026-09-15 08:41:51 +08:00
- patrol loop
- patrol mode
- delayed wake
2026-09-16 13:13:18 +08:00
# 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]).
2026-09-15 08:41:51 +08:00
- abort session
- revive session
- session rescue
- fact baseline
2026-09-16 13:13:18 +08:00
# English — quota-wall recovery ([org-internal #3669]): worker killed by provider usage limit.
2026-09-15 08:41:51 +08:00
- quota wall
- usage limit reached
- quota exhausted
- switch provider
2026-09-16 13:13:18 +08:00
# Chinese — lifecycle ops ([org-internal #2459]).
- 唤醒会话
- 会话挂死
- 事实基线
# Chinese — quota-wall recovery ([org-internal #3669]).
- 配额墙
- 配额耗尽
- 用量上限
- 换供应商
2026-09-15 08:41:51 +08:00
role : Producer
---
2026-09-16 13:13:18 +08:00
> Core 中立版(Increment 6a 改写,原 deferHard verbatimDir)。机制、结构与 frontmatter 保持;实例术语(工具名、路径、工单号)按 `core/adapters/TERMINOLOGY.md` 绑定到具体实例。
2026-09-15 08:41:51 +08:00
# Headless Main Session Launch (prompt_async)
Launch a **main session for a specific issue / work item from a headless
2026-09-16 13:13:18 +08:00
context** — no Web UI, no TUI, just HTTP. This is the "fire-and-forget" path: an
2026-09-15 08:41:51 +08:00
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.
2026-09-16 13:13:18 +08:00
## Directory convention — independent session in the current workspace, no worktree
2026-09-15 08:41:51 +08:00
The default is an **independent session sharing the current session's
2026-09-16 13:13:18 +08:00
directory** — the repo you are already working in. No new worktree, no
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
makes when it starts actual work — not something the launcher does. One
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
**repo** — never a worktree of the same repo. The Web UI sidebar groups
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
is wherever the server was started — unreliable. In that context `?directory=`
2026-09-15 08:41:51 +08:00
remains effectively required.
2026-09-16 13:13:18 +08:00
- **Dispatch convention — directory = code location, not tracker repo.** When
2026-09-15 08:41:51 +08:00
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: `<owner>/<backend-repo>#<n>` is filed in
2026-09-16 13:13:18 +08:00
the ticket-tracker repo but edits the main repo's workflow files → the
2026-09-15 08:41:51 +08:00
session's `?directory=` is the **code checkout root**
(`<workspace-root>/<org>/<repo>` ), not the tracker repo checkout. Never point
2026-09-16 13:13:18 +08:00
`?directory=` at a ticket worktree — the worktree goes in the claim comment +
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
the owning main repo — if you see that header, the session will be invisible
2026-09-15 08:41:51 +08:00
to `GET /session?directory=<mainRepo>` owner-checks; re-check your launch
parameters before proceeding.
> **Authoritative reference:** `rules/headless-session-ops` wiki page
> (<<instance-base-url>/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 `<harness-package>` at HEAD.
2026-09-16 13:13:18 +08:00
## The `model` field — always provide a known-good model (read this first)
2026-09-15 08:41:51 +08:00
`prompt_async` is **fire-and-forget** : after HTTP returns 204 (or 202 when the
2026-09-16 13:13:18 +08:00
message is queued behind a wedge/zombie session — no live runner in that
2026-09-15 08:41:51 +08:00
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 (`<harness-package>/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
` ``
2026-09-16 13:13:18 +08:00
So omission does **not** produce a clean validation error — it silently
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
caller — so the session looks healthy (HTTP 204, user message persisted) yet
2026-09-15 08:41:51 +08:00
**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 |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
2026-09-16 13:13:18 +08:00
| ` {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). |
2026-09-15 08:41:51 +08:00
> **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**
2026-09-16 13:13:18 +08:00
model uses ` { id, providerID, variant }` (` session.ts:256-260`) — i.e. ` modelID`
2026-09-15 08:41:51 +08:00
is called ` id`. When you reuse a model read from a session record, remap
2026-09-16 13:13:18 +08:00
` model.id → modelID` (see recipe below). User-message ` info.model` already uses
2026-09-15 08:41:51 +08:00
` modelID` (` message.ts:403-407`).
## Reuse the current session's model (recommended default)
2026-09-16 13:13:18 +08:00
The model powering the launching session is proven to work — reuse it. Discover
2026-09-15 08:41:51 +08:00
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}**)."
2026-09-16 13:13:18 +08:00
e.g. ` modelID = "glm-5.2"`, ` providerID = "zai-coding-plan"`(曾名
zhipuai-coding-plan, 2026-08 更名)
2026-09-15 08:41:51 +08:00
- **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" \
2026-09-16 13:13:18 +08:00
| jq '.model' # → { "id": "glm-5.2", "providerID": "zai-coding-plan", "variant": ... }
2026-09-15 08:41:51 +08:00
` ``
2026-09-16 13:13:18 +08:00
Then **remap** ` model.id → modelID` when building the payload.
2026-09-15 08:41:51 +08:00
- **Verify the provider is actually connected** before launching (cheap
2026-09-16 13:13:18 +08:00
insurance — the model only generates if its provider is live):
2026-09-15 08:41:51 +08:00
` ``sh
curl -s "$BASE/provider?directory=$DIR" | jq '.connected | index("<current providerID>")'
2026-09-16 13:13:18 +08:00
# → null = provider NOT connected (do NOT launch); 0..n = connected
2026-09-15 08:41:51 +08:00
` ``
or call the ` list-models` tool. Note: model-level availability is only
2026-09-16 13:13:18 +08:00
knowable at runtime — a _configured_ model can still fail (subscription-tier
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
session's directory for in-session launches — see the Directory convention
2026-09-15 08:41:51 +08:00
above; only specify a different one explicitly when targeting another repo.
2026-09-16 13:13:18 +08:00
- A known-good ` model` (reuse the current session's — see above). Model IDs
2026-09-15 08:41:51 +08:00
must match the provider config in ` <config-home>/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
` ``
2026-09-16 13:13:18 +08:00
### Step 1 — Create the session
2026-09-15 08:41:51 +08:00
` ``sh
SID=$(curl -s -X POST "$BASE/session?directory=$DIR" \
-H 'content-type: application/json' \
2026-09-16 13:13:18 +08:00
-d '{"title":"#N — <short description>","agent":"builder"}' | jq -r .id)
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
the **current session's directory** (Directory convention above) — keep it
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
repo is NOT a directory target — hand it over via the seed prompt), and
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
spawned within a session — never named here.
2026-09-15 08:41:51 +08:00
2026-09-16 13:13:18 +08:00
### Step 2 — Deliver the seed prompt (CRITICAL: include a known-good ` model`)
2026-09-15 08:41:51 +08:00
` ``sh
curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \
-H 'content-type: application/json' \
-d '{
"agent": "builder",
"model": { "providerID": "<current providerID>", "modelID": "<current modelID>" },
"parts": [ { "type": "text", "text": "<seed prompt: restate goal, constraints, acceptance criteria, cite wiki artifact paths>" } ]
}'
` ``
- 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
2026-09-16 13:13:18 +08:00
process (wedge/zombie shape — stored and re-driven on restart drain or the
2026-09-15 08:41:51 +08:00
next runner), **202** when ` delay_sec` defers delivery to an in-memory
2026-09-16 13:13:18 +08:00
delayed wake ([org-internal #3937] A — see "巡检模式 / Patrol mode" below), and **409**
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
delegates to the same delivery core — same 204/202/409 semantics, same
2026-09-15 08:41:51 +08:00
` delay_sec` contract, body is the session-scoped payload plus a required
2026-09-16 13:13:18 +08:00
` sessionID` field. Unknown sessionID → 404 naming the session-scoped
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
stripped — retention semantics win (the server logs the drop; the delay is
NOT honored). Pending wakes are **in-memory only — a server restart abandons
2026-09-15 08:41:51 +08:00
them**; a patrol loop must therefore tolerate a missed wake and re-arm.
2026-09-16 13:13:18 +08:00
Never use ` sleep <big>; curl ...` in a bash tool call to wait — that blocks
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
` "providerID/modelID"` (split on the FIRST ` /`; accepted since [org-internal #4307] —
2026-09-15 08:41:51 +08:00
e.g. ` "zai-coding-plan/glm-5.2"`). **Do not omit it.** Fill the values from
2026-09-16 13:13:18 +08:00
your own session's model (see "Reuse the current session's model" above) —
2026-09-15 08:41:51 +08:00
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:"<current providerID>",modelID:"<current modelID>"},parts:[{type:"text",text:$p}]}'
` ``
2026-09-16 13:13:18 +08:00
### Step 3 — Verify the session actually generated (do NOT assume success)
2026-09-15 08:41:51 +08:00
2026-09-16 13:13:18 +08:00
HTTP 204/202 ≠generation success. Poll until an ` assistant` message appears. The
endpoint is **` /session/:id/message` — SINGULAR**, and it returns a bare array
2026-09-15 08:41:51 +08:00
whose elements are ` { info, parts }` with ` role` at ` .info.role`
2026-09-16 13:13:18 +08:00
(` 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
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
# wait 5– 10s, then poll the TAIL — never the full transcript
2026-09-15 08:41:51 +08:00
curl -s "$BASE/session/$SID/message?directory=$DIR&limit=1" \
| jq '{latest_role: .[0].info.role, latest_id: .[0].info.id}'
` ``
2026-09-16 13:13:18 +08:00
Verdict: ` latest_role == "assistant"` → the first generation completed (the
2026-09-15 08:41:51 +08:00
seed is ` user`; the newest message flips to ` assistant` only once the model
2026-09-16 13:13:18 +08:00
answered — an errored turn also persists an assistant message carrying the ⚠️
2026-09-15 08:41:51 +08:00
usage-limit part, [org-internal #2912], so check the tail text before declaring success).
2026-09-16 13:13:18 +08:00
Stays ` user` across 2– 3 polls spaced 10– 30 s → **apparent silent stall**;
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
` after=<cursor>&limit=N` and follow the ` X-Next-Cursor` response header —
2026-09-15 08:41:51 +08:00
just never omit ` limit`.
2026-09-16 13:13:18 +08:00
> ⚠️ Do NOT write ` /messages` (plural) — that path does not exist for listing.
2026-09-15 08:41:51 +08:00
> (` 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
2026-09-16 13:13:18 +08:00
tracked issue — seed prompt composed **from the issue body**, launched on the
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
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` /
2026-09-15 08:41:51 +08:00
` 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).
2026-09-16 13:13:18 +08:00
## Completion-report protocol (完成回报协议, [org-internal #2374])
2026-09-15 08:41:51 +08:00
` 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.
2026-09-16 13:13:18 +08:00
Default report format is ` branch=<ref>`-based — workers never open PRs
2026-09-15 08:41:51 +08:00
(TD-678/[org-internal #4425]); ` pr=#N ` appears only on ` uncoordinated` self-opens.
Hard constraints: a headless worker MUST NOT call the ` question` tool
2026-09-16 13:13:18 +08:00
([org-internal #2378] — directional/irreversible decisions are ` status=blocked` reports,
2026-09-15 08:41:51 +08:00
then STOP), and MUST NOT self-merge its PR / self-close the issue / touch
2026-09-16 13:13:18 +08:00
` main` ([org-internal #2386] — keep the PR open; the orchestrator merges and closes). Full
protocol — orchestrator-side seed block (verbatim template), worker-side
2026-09-15 08:41:51 +08:00
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])
2026-09-16 13:13:18 +08:00
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`
2026-09-15 08:41:51 +08:00
+ message count); ` state=generating` with ALL counters frozen across BOTH
2026-09-16 13:13:18 +08:00
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
2026-09-15 08:41:51 +08:00
injection (only the CURRENT authority injects; if YOU might be the stale one,
2026-09-16 13:13:18 +08:00
verify identity against the durable record first). Full runbooks — verdict
2026-09-15 08:41:51 +08:00
table + [org-internal #3215] dual-sample rule, abort + re-wake sequence, fact-baseline
structure, authorization asymmetry: ` reference/lifecycle-ops.md` (read when
2026-09-16 13:13:18 +08:00
managing a launched session). Quota wall — the worker's turn killed by a
2026-09-15 08:41:51 +08:00
terminal usage-limit error (looks like silent completion: no report, ticket
2026-09-16 13:13:18 +08:00
stalled) — has its own runbook: detection signals (finish=error ⚠️ tail,
2026-09-15 08:41:51 +08:00
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).
2026-09-16 13:13:18 +08:00
## 巡检模式 / Patrol mode (delayed self-wake, [org-internal #3937])
2026-09-15 08:41:51 +08:00
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": "<current providerID>", "modelID": "<current modelID>" },
"parts": [ { "type": "text", "text": "<patrol instruction>" } ],
"delay_sec": 3600
}'
` ``
Rules of the road:
2026-09-16 13:13:18 +08:00
- **202 + ` delay_sec` ≠queued-behind-wedge 202.** Both are 202, but the
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
omission → immediate 204 path). If the session was NOT idle at intake, the
2026-09-15 08:41:51 +08:00
intake keeps the queued semantics and ` delay_sec` is ignored (logged
2026-09-16 13:13:18 +08:00
server-side) — the same precedence as maintenance queue mode, where the
2026-09-15 08:41:51 +08:00
intake is retained and ` delay_sec` never delays the flush.
2026-09-16 13:13:18 +08:00
- **Paused at wake → dropped, not stored.** The wake's ` prompt()` rejects and
2026-09-15 08:41:51 +08:00
the server publishes an error event; the message is never stored. A patrol
driver should check session state before assuming delivery.
2026-09-16 13:13:18 +08:00
- **Restart abandons pending wakes.** They are in-memory server fibers — no
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
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
2026-09-15 08:41:51 +08:00
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 ` <shell_warning>` on bare sleeps
2026-09-16 13:13:18 +08:00
beyond 120s ([org-internal #3937] C) — treat that warning as a redirect to this section.
2026-09-15 08:41:51 +08:00
## Do NOT use this skill when
2026-09-16 13:13:18 +08:00
- A **persistent client** (Web UI / interactive TUI) is attached — those use the
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
the UI) — abort + re-wake targets headless / managed sessions only.
- You want the response **inline** in the same HTTP call — ` prompt_async` is
2026-09-15 08:41:51 +08:00
fire-and-forget; use the synchronous ` prompt` endpoint instead.
## Common pitfalls
| # | Pitfall | Fix |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2026-09-16 13:13:18 +08:00
| 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`) |
2026-09-15 08:41:51 +08:00
| 4 | Stale ` octopus` binary rejects newer config schema (` Unrecognized key`) | Use the source CLI ` bun run ./src/index.ts ...`; the config itself is valid |
2026-09-16 13:13:18 +08:00
| 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` |
2026-09-15 08:41:51 +08:00
| 7 | Shell-escaping errors in a multi-line seed prompt | Build the payload with ` jq -n --rawfile` from a file |
2026-09-16 13:13:18 +08:00
| 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=<ref>`; 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 |
2026-09-15 08:41:51 +08:00
## 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
2026-09-16 13:13:18 +08:00
hard-require ` model` — it fills it via a fallback chain (` prompt.ts:858`) and any
2026-09-15 08:41:51 +08:00
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
2026-09-16 13:13:18 +08:00
provide an explicit known-good ` model`** — this skill is the documented
workaround (see [org-internal #1695] "遗留 minor").
2026-09-15 08:41:51 +08:00
**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
2026-09-16 13:13:18 +08:00
assistant message persists ` finish=error` plus a human-readable ⚠️ text part
2026-09-15 08:41:51 +08:00
carrying provider, limit reason, and reset hint (` metadata: { error: true,
2026-09-16 13:13:18 +08:00
reason, provider }`) — visible via ` GET /session/:id/message` and rendered in
2026-09-15 08:41:51 +08:00
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).
2026-09-16 13:13:18 +08:00
- Quota-wall recovery recipe: **[org-internal #3669]** (evidence instance [org-internal #3627]) — detect a
2026-09-15 08:41:51 +08:00
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,
2026-09-16 13:13:18 +08:00
self-close the issue, or touch ` main` — the orchestrator merges and closes
2026-09-15 08:41:51 +08:00
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: ` <harness-package>/src/session/prompt.ts:858,804-822,1399`,
` handlers/session.ts` ` forkPromptDelivery` (async failure handling),
` server/routes/instance/httpapi/groups/session.ts`.
## References
2026-09-16 13:13:18 +08:00
**On-demand references** (NOT injected — read at the declared timing):
2026-09-15 08:41:51 +08:00
2026-09-16 13:13:18 +08:00
- ` reference/ticket-recipe.md` — ticket-driven launch (` Launch a session for a
specific ticket (#N )`): read BEFORE creating any session for issue #N —
2026-09-15 08:41:51 +08:00
Step-0 owner check, seed-prompt composition, launch + poll commands.
2026-09-16 13:13:18 +08:00
- ` reference/completion-report.md` — completion-report protocol (完成回报协议,
2026-09-15 08:41:51 +08:00
[org-internal #2374]): read BEFORE composing a seed prompt for a worker you need to hear
2026-09-16 13:13:18 +08:00
back from — orchestrator block template, worker-side report steps, decision
2026-09-15 08:41:51 +08:00
boundary ([org-internal #2378] / [org-internal #2386]), quota self-report clause ([org-internal #3669]).
2026-09-16 13:13:18 +08:00
- ` reference/lifecycle-ops.md` — post-launch lifecycle ([org-internal #2459]): read when
managing a launched session — health-check polling, hung-stream diagnosis
2026-09-15 08:41:51 +08:00
(state=generating + frozen counters), abort + re-wake rescue,
fact-baseline injection, authorization asymmetry.
2026-09-16 13:13:18 +08:00
- ` reference/quota-wall-recovery.md` — quota-wall recovery ([org-internal #3669], evidence
2026-09-15 08:41:51 +08:00
[org-internal #3627]): read when a worker goes silent mid-task (suspected provider usage
2026-09-16 13:13:18 +08:00
limit) or BEFORE dispatching a long-running worker — detection signals
(finish=error ⚠️ tail, provider quota markers, log grep patterns),
2026-09-15 08:41:51 +08:00
provider-switch re-wake + fact-baseline template, pre-dispatch provider
check, quota self-report clause.
2026-09-16 13:13:18 +08:00
- ` rules/headless-session-ops` wiki page — authoritative reference (this
2026-09-15 08:41:51 +08:00
skill is its runtime carrier; link in the Directory-convention note above).
2026-09-16 13:13:18 +08:00
- ` core/rules/session-scope-guard.md` — owner-check + claim-first rule
2026-09-15 08:41:51 +08:00
(the ticket recipe's Step 0 is its launch-time application).
2026-09-16 13:13:18 +08:00
- ` core/rules/compact.md` — identity-verification clause backing the
2026-09-15 08:41:51 +08:00
authorization-asymmetry rule (` reference/lifecycle-ops.md`).