Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,22 @@ Configure: `set-mode` (`build|plan|review`), `set-model`, `set-effort` (`auto|lo

Work: `prompt`, `follow`, `watch`, `steer`, `queue`, `discard-queued`, `exec`, `cancel`, `runtime`

`prompt --new TEXT` creates a session. `prompt SESSION_ID TEXT` continues one. `-` reads the prompt from stdin. `--delivery auto` prompts when idle, steers when the turn is steerable, otherwise queues. It waits until idle, including queued turns.
`prompt --new TEXT` creates a session. `prompt SESSION_ID TEXT` continues one. `-` reads the prompt from stdin. `--delivery auto` prompts when idle, steers when the turn is steerable, otherwise queues. Auto delivery and idle waits require the updated backend observation metadata; unknown/unavailable status is not usable.

Prompt/follow/watch wait for self-session idle, including queued turns, not for
child workflows or detached processes. Idle is not task success. Idle results
retain runtime observation health and separate direct delegation activity.

`queue SESSION_ID -` and `steer SESSION_ID -` read stdin and return a submission
receipt without loading history. `prompt --delivery queue|steer --until-idle false`
also returns after submission; idle/direct prompt delivery still waits for its turn.

`discard-queued` takes the `inputId` from an `input_state` event. That is the same value as `clientInputId` from the submit response.

`cancel` sends a cancellation notification for the current turn. Its
`cancellationState: "notification_sent"` confirms the local write/flush, not run
termination or task success. It does not discard queued inputs or cancel children.

`find` reports `pagesScanned`, `truncated`, `pageCapReached`, and `stopReason`
so callers can distinguish a complete search from a result/page limit.

Expand Down Expand Up @@ -102,7 +110,11 @@ a final `type: done` result. `watch` also emits `runtime` events.

Exit codes: 0 ok, 1 usage, 2 connection, 3 RPC, 4 timeout or cancel.

Event types: `session`, `submitted`, `text`, `tool`, `mode`, `plan`, `input_state`, `permission`, `elicitation`, `runtime`, `update`, `done`.
Event types: `session`, `submitted`, `text`, `tool`, `delegation`, `mode`, `plan`, `input_state`, `permission`, `elicitation`, `runtime`, `update`, `done`.

`delegation` records describe workflow lifecycle separately from dispatch `tool`
status. Compact `inspect` includes snapshot `delegations`; successful dispatch
alone does not establish delegation success.

Submission receipts retain `clientInputId`; partial-failure errors include known
IDs and delivery state in `recovery`. Acceptance is not task completion. See
Expand Down
8 changes: 6 additions & 2 deletions skills/qmacp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ keep the returned `sessionId`. Default endpoint: `ws://127.0.0.1:3030/acp/ws`.
`--permission cancel` to fail closed. Elicitation is always cancelled.
- JSON/NDJSON goes to stdout; connection logs go to stderr. Require exit 0 and
JSON `ok` not false; `prompt`/`follow` also need a terminal `"type":"done"`.
Do not use `--pretty` on streams. Acceptance is not proof of task completion.
Do not use `--pretty` on streams. Acceptance and self idle are not task success.
`cancel` with `cancellationState: "notification_sent"` confirms only sending the
notification, not run termination.
- After a timeout or uncertain submission, inspect `runtime` and history before
retrying, steering, queuing, discarding, or cancelling. Do not claim changes
succeeded without exit 0 and JSON `ok: true`. Retain receipts and error
Expand All @@ -52,7 +54,9 @@ qmacp --quiet runtime "$SESSION_ID"

Long prompts: use stdin (`-`), not nested quoting. Make them self-contained.
Existing-session `prompt` defaults to `--delivery auto`: idle -> prompt,
busy and steerable -> steer, otherwise queue. It waits until idle by default.
busy and steerable -> steer, otherwise queue. Auto delivery and idle waits require
the updated backend observation contract; unknown/unavailable is not a usable
phase. Waiting means self-session idle, not descendant/process completion.
Read [live control](control.md) for delivery, waiting, cancellation, and how to
label agent-to-agent messages.

Expand Down
7 changes: 4 additions & 3 deletions skills/qmacp/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ notify_callback() {
steering; `steer --run-id` can target an exact active run. Do not assume
queue-only delivery wakes an idle session. See [live control](control.md)
for direct commands, including stdin delivery without history loading.
- `--until-idle false` avoids waiting for all subsequent work after
steer/queue. It is **not** fire-and-forget for an idle session: the prompt
RPC still waits for that turn, bounded here by `--timeout 900`. Keep the
- `--until-idle false` skips the self-session idle wait after steer/queue.
That wait never implies descendant workflow or detached process completion.
It is **not** fire-and-forget for an idle session: the prompt RPC still waits
for that turn, bounded here by `--timeout 900`. Keep the
delivery process supervised.
- Keep stdout NDJSON and stderr in the watcher logs; check exit status and the
final `done` event. Submission/`done` alone does not prove the agent handled
Expand Down
39 changes: 30 additions & 9 deletions skills/qmacp/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,50 @@ qmacp [--url URL] [--allow-insecure] [--pretty] [--permission allow-once|reject-
- `prompt`, `follow`, `exec`, and opt-in `new --stream`: NDJSON.
`watch` emits `runtime` events, then a final JSON object.
Connection logs go to stderr.
- Success requires exit 0 and JSON `ok` not false. `prompt`, `follow`, and
- Command success requires exit 0 and JSON `ok` not false. `prompt`, `follow`, and
`new --stream` also require a terminal `"type":"done"`. `exec` emits individual
command results, not a global `done`.
- Exit codes: 0 success, 1 usage, 2 connection, 3 RPC, 4 timeout/cancel.

Stream event types: `session`, `submitted`, `text`, `tool`, `mode`, `plan`,
`input_state`, `permission`, `elicitation`, `runtime`, `update`, `done`.
Do not claim a cancel/discard/close/config change succeeded without exit 0 and
JSON `ok: true`. Inspect current state after uncertain delivery rather than retrying
blindly; see [live control](control.md).
Stream event types: `session`, `submitted`, `text`, `tool`, `delegation`, `mode`,
`plan`, `input_state`, `permission`, `elicitation`, `runtime`, `update`, `done`.
`delegation` is a separate lifecycle record from `querymt/session/delegationUpdate`,
also delivered by the exported `AcpClient::connect_with_events` channel. It retains
backend identities, state, child/tool links, stage/activity, outcome details and
`sourceEpoch`/`sessionSeq` when supplied, without inferred defaults. A completed
`tool` can mean only successful dispatch while its delegation is still verifying
or later fails. Delegation ordering is separate from self-runtime revisions;
records are forwarded, not merged into a client-side state cache.

Require exit 0 and JSON `ok: true` for command success. For `cancel`,
`cancellationState: "notification_sent"` confirms only the local notification
write/flush, not backend acceptance or execution termination. Inspect current
state after uncertain delivery rather than retrying blindly; see [live control](control.md).

`new --stream`/`prompt --new` emit `sessionId` before configuration. Default
`new` still returns one object. Queue/steer prompts emit `submitted` with
`clientInputId` and `receipt` before waiting; `done` retains them.

On failure, `recovery` preserves known IDs, the failing `stage`
(`create|configure|submit|observe`), and creation/delivery state. `not_submitted`
(`create|configure|submit|open|observe`), and creation/delivery state. `not_submitted`
or `rejected` differs from `unknown`; `accepted` means a receipt was received,
not that the task completed. Inspect state before retrying an uncertain or
accepted submission. A lost creation response cannot provide a session ID.

`observationState` is `not_waited`, `idle`, or `prompt_returned`, not proof of
task success. `text` contains only assistant text observed on this connection.
Successful prompt `observationState` is `not_waited`, `idle`, or `prompt_returned`;
follow reports `idle`. None proves task success. Idle results retain `runtime`,
including backend observation health, run identity and direct delegation activity.
Self idle does not mean descendants finished or detached OS processes exited.
The prompt RPC `stopReason` is separate from idle evidence; it is also retained in
recovery when observation fails after the RPC returned. It does not describe a
later queued run. No terminal outcome is inferred from idle.

Open/observation failures emit no successful `done`; recovery reports
`observationState: "unavailable"` with the session ID and runtime evidence when
available. A retained runtime may be the last known observation, not current
liveness. Updated backend observation metadata is required for auto delivery and
waits; absent/unknown/unavailable health is not a usable busy or idle state.
`text` contains only assistant text observed on this connection.

## Permissions

Expand Down
32 changes: 23 additions & 9 deletions skills/qmacp/control.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ self-contained; elicitation is always cancelled. Existing sessions accept
- `queue`: submit input for after the current turn. Whether it starts an idle
session depends on server support; use `auto` when that support is unknown.

`--until-idle` defaults true and waits through queued turns. Pass `--timeout`
explicitly for predictable bounds. `--until-idle false` skips waiting for idle
following steer/queue; it is not fire-and-forget when idle delivery uses the
prompt RPC, which still waits for that turn.
`--until-idle` defaults true and waits for this session's own idle state, including
queued turns. It does not wait for descendant workflows or detached OS processes,
and idle is not task success. Pass `--timeout` explicitly for predictable bounds.
`--until-idle false` skips waiting for idle following steer/queue; it is not
fire-and-forget when idle delivery uses the prompt RPC, which still waits for
that turn.

Queue/steer delivery skips history loading. Direct commands return a receipt;
`prompt --until-idle false` returns after submission when either is selected.
Expand Down Expand Up @@ -62,21 +64,33 @@ qmacp --quiet cancel "$SESSION_ID"
qmacp --quiet close "$SESSION_ID"
```

- `follow` streams session events until idle and loads cwd from the current
process; run it from the session workspace.
- `watch` polls `runtime` (`"type":"runtime"`) until idle unless `--idle false`.
Default `follow`/`watch` timeout is 120s.
- `follow` requires successful session open, then streams events until self idle;
run it from the session workspace (cwd comes from the current process).
- `watch` polls `runtime` (`"type":"runtime"`) until self idle unless `--idle false`.
Default `follow`/`watch` timeout is 120s. Unknown/unavailable observation fails
rather than completing either command, including nonwaiting watch.
- Auto delivery and waits require the updated backend `observation.availability`
contract (`live` or known `inactive`); a bare phase is not usable evidence.
`runtime.delegation_activity` is separate direct-workflow activity and may remain
live after self idle. Prompt/follow idle results retain the full `runtime`.
Live `delegation` events report workflow lifecycle separately from dispatch
`tool` events; neither changes follow's self-idle completion scope.
- `steer --run-id` is optional; if omitted, qmacp pins `runtime.active_run_id`.
Missing/empty run IDs fail before submission rather than steering unguarded.
- Queued submit returns `clientInputId`. `input_state` events expose the same ID
as `inputId`; pass that exact value to `discard-queued`.
- `cancel` returns `cancellationState: "notification_sent"` after the local
WebSocket write/flush. It does not confirm backend acceptance or run termination,
discard queued inputs, or cancel child workflows. Inspect `runtime` separately;
a queued next run can keep the session busy.
- Do not close a session unless the user asked to end it or a qmtcode test
explicitly requires `close`.

After a timeout, inspect `runtime` and session history before resending or
changing work. Do not steer, queue, discard, or cancel just because time ran out.
Require exit 0 and JSON `ok` not false; prompt/follow also need a terminal
`"type":"done"`. Require JSON `ok: true` for cancel/discard/close. A submission
`"type":"done"`. For cancel, exit 0 and JSON `ok: true` prove only notification
sending; for discard/close they confirm the respective operation. A submission
receipt is not proof the agent completed the task.

For event-driven waiting outside the agent turn, see [callbacks](callbacks.md).
11 changes: 10 additions & 1 deletion skills/qmacp/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ creation receipts, opt in to `new --stream`; see [CLI output](cli.md#output).

`find` reports `pagesScanned`, `truncated`, `pageCapReached`, and `stopReason`
(`exhausted|limit|page_cap`). A truncated empty result is not exhaustive.
Summaries retain full backend `runtime` observation/activity metadata. Missing
phase stays null; unknown/unavailable or unobserved status never becomes idle.
`find --phase idle` matches only usable self-idle observations, even when managed
dependency activity remains live. It does not test task or descendant completion.

## Inspect and configure

Expand All @@ -52,7 +56,12 @@ qmacp --quiet set-effort "$SESSION_ID" high # auto|low|medium|high|max
```

`status` is mode/model/profile/effort; `runtime` is phase/queue/steerability.
`inspect --tools` defaults true; use `--tools false` for compact history.
Compact `inspect` includes `delegations` from snapshot `delegationUpdates`, separate
from dispatch `tools`, using the same records as live `delegation` events.
`inspect --tools` defaults true; `--tools false` hides only tools, not delegations.
Snapshot records are point observations; a later live record with the same
`sourceEpoch` and higher `sessionSeq` is newer. Historical nonterminal rows do not
prove live activity, and a terminal workflow record does not prove OS process exit.
`--full` dumps the raw snapshot (large). Do not treat earlier output as current
state; query again before acting. Require exit 0 and JSON `ok: true` for changes.

Expand Down
8 changes: 6 additions & 2 deletions skills/qmacp/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ requirements; do not alter unrelated sessions or user-run services.
- Load/list features: if `caps.acp.loadSession`/`list`/`close` are false, do not keep
calling `inspect`/`sessions`/`close`.

Query `runtime` or `status` again rather than treating earlier output as current.
Do not claim cancel/discard/close/config success without exit 0 and JSON `ok: true`.
Query `runtime` again for execution state rather than treating earlier output as
current; `status` is configuration only. Auto delivery and waits require updated
backend observation metadata. Unknown/unavailable is not idle or task success.
Require exit 0 and JSON `ok: true` for command success. Cancel's
`cancellationState: "notification_sent"` proves only notification write/flush;
check runtime/outcome evidence separately for termination.
Prompt/follow checks also require a terminal `"type":"done"`, but a receipt or idle
state alone is not semantic proof of the task result. Inspect the relevant result.
8 changes: 8 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,14 @@ async fn handle_inbound(
event_sink.emit(event).await;
}

if matches!(
method.as_str(),
"querymt/session/delegationUpdate" | "_querymt/session/delegationUpdate"
) && let Some(event) = events::compact_delegation_update(&params)
{
event_sink.emit(event).await;
}

if let Some(id) = value.get("id").cloned() {
let (reply, event) = inbound_request_reply(id, &method, &params, permission)?;
if let Some(event) = event {
Expand Down
Loading
Loading