47 KiB
name, description, triggers, role
| name | description | triggers | role | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| headless-session-ops | 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`. |
|
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
DIRto your own session'sdirectory(you already know it from context, or read.directoryfrom your own session record viaGET /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.tsxper-workspaceworkspaceSessions(directory), fetched viasession.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'sprocess.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:<owner>/<backend-repo>#<n>is filed in the ticket-tracker repo but edits the main repo's workflow files → the session's?directory=is the code checkout root (<workspace-root>/<org>/<repo>), 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 anX-Session-Directory-Warningresponse header and a server warn log naming the owning main repo — if you see that header, the session will be invisible toGET /session?directory=<mainRepo>owner-checks; re-check your launch parameters before proceeding.
Authoritative reference:
rules/headless-session-opswiki 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<harness-package>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:
-
Current code does not hard-require
model. The HTTP boundary marksmodelas optional (<harness-package>/src/session/prompt.ts:1860), andcreateUserMessagefills a missing model via a fallback chain (prompt.ts:858):const model = input.model ?? ag.model ?? yield * currentModel(input.sessionID) // ^^^^^^^^^ agent ^^^^^^^^^^^^^^^^^ session/defaultSo 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). -
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
getModelraisesModelNotFoundError(prompt.ts:1399,:804-822). In the async handler that error is caught by the delivery fork's failure handling (forkPromptDeliveryinhandlers/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 frozentime.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_asyncpayload MUST carry a known-goodmodel, 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):
GETthe launching session's record and read.model:curl -s "$BASE/session/$LAUNCHER_SID?directory=$DIR" \ | jq '.model' # → { "id": "glm-5.2", "providerID": "zai-coding-plan", "variant": ... }Then remap
model.id → modelIDwhen building the payload. -
Verify the provider is actually connected before launching (cheap insurance — the model only generates if its provider is live):
curl -s "$BASE/provider?directory=$DIR" | jq '.connected | index("<current providerID>")' # → null = provider NOT connected (do NOT launch); 0..n = connectedor call the
list-modelstool. Note: model-level availability is only knowable at runtime — a configured model can still fail (subscription-tier or region errors);GET /config/providerslists 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 noGET /modelHTTP 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<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:
BASE=http://127.0.0.1:4096
DIR=/data/octopus
Step 1 — Create the session
SID=$(curl -s -X POST "$BASE/session?directory=$DIR" \
-H 'content-type: application/json' \
-d '{"title":"#N — <short description>","agent":"builder"}' | jq -r .id)
echo "session=$SID"
POST /sessionreturnsSession.Infowith.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 ownprocess.cwd()otherwise).agentisbuilder(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)
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 process (wedge/zombie shape — stored and re-driven on restart drain or the next runner), 202 whendelay_secdefers 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'ssessionIDand delegates to the same delivery core — same 204/202/409 semantics, samedelay_seccontract, body is the session-scoped payload plus a requiredsessionIDfield. 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 afterdelay_secseconds, only for an idle session at intake. Precedence ([org-internal #3937] review):delay_secapplies 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 usesleep <big>; 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).modelis{ 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:
jq -n --rawfile p seed.txt '{agent:"builder",model:{providerID:"<current providerID>",modelID:"<current 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()):
# 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=<cursor>&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=<ref>-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=generatingwith 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.
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:
- 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;0equals omission → immediate 204 path). If the session was NOT idle at intake, the intake keeps the queued semantics anddelay_secis ignored (logged server-side) — the same precedence as maintenance queue mode, where the intake is retained anddelay_secnever 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_secon 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<shell_warning>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
promptendpoint (it blocks until generation completes) and sendmodelautomatically. Usingprompt_asyncthere 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_asyncis fire-and-forget; use the synchronouspromptendpoint 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_asyncs 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 |
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 throughstatus=blockedreports to the orchestrator) and [org-internal #2386] (the worker MUST NOT self-merge its PR, self-close the issue, or touchmain— 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:
<harness-package>/src/session/prompt.ts:858,804-822,1399,handlers/session.tsforkPromptDelivery(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-opswiki 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).