Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/patch-cli-obsolete-work-cancellation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions docs/src/content/docs/reference/work-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ Repository selection is `--repo`, then `GH_REPO`, then local `origin`, then `gh
| `deploy --from-config` | Refresh compatible worker routes; optional `--pool`, `--worker-profile`. |
| `policy --from-config --epoch EPOCH` | Update existing, quiescent Policy; `--file` is the mutually exclusive alternative. |
| `control`, `cancel-work` | Apply authorized pause, cutover, or Work cancellation. |
| `cancel --obsolete [--limit N] [--dry-run]` | Select one checked obsolete-contract batch; `cancel` is an alias of `cancel-work`. |
| `cancel-claim --claim-id ID[,ID...] --reason CODE` | Cancel exact current Claims and their Work, not native runs. |
| `reprioritize --work-id ID[,ID...] --priority 1..5 --reason CODE` | Administrator-only priority override for future grants of available Work. |
| `clear --reason CODE [--dry-run] [--hard]` | Interactive local cancellation or settled-queue reset; see below. |
Expand All @@ -265,6 +266,21 @@ All timestamps are Unix milliseconds. Inspection reads do not refresh external d

Metadata diagnostics exclude payloads, Result descriptors, request parameters, and receipt bodies. Retired pools remain visible with `policy_installed: false`; unknown admission ages are counted rather than inferred from client timestamps.

### Cancel obsolete worker-contract Work

```bash
gh aw work --repo OWNER/REPO cancel --obsolete \
--limit 256 --request-id obsolete-batch-1 --dry-run --json
```

`cancel` aliases `cancel-work`. `--obsolete` uses the JavaScript maintenance selection: only available, unclaimed, unpinned Work with barrier `none` and an admission contract incompatible with an available approved active Deployment is eligible. Expired Work, compatible Work, explicit execution pins, and missing/unavailable deployments are excluded. IDs are sorted in UTF-8 byte order; one batch contains at most `--limit` Work (default and maximum 256), further bounded by the installed Policy operation limit. It does not refresh routes or restore missing deployments.

`--dry-run` reads and validates the ledger and reports the selection without checking administrator publication authority, recording a request, synchronizing Issues, or causing queue effects. Remove it to publish an administrator-authorized atomic `cancel_work` request with fixed reason `worker_contract_obsolete` and operation `version: 1`. Fresh publication checks reject the whole batch if a selected Work becomes owned, expired, compatible, or otherwise ineligible. Claims, Results, delivery barriers, reservations, policy/epoch/limits, fairness debt, history, and unselected Work are preserved; no native runs are stopped.

`--work-id` and `--reason` cannot be combined with `--obsolete`; `--limit` and `--dry-run` require it. Use the same stable `--request-id` after an uncertain publication. An accepted request recovers only its original cancellation targets and fingerprint, including historical legacy receipts; it never sweeps newly obsolete Work. A smaller limit that cannot hold the original receipt is rejected. To select another batch, use a new request ID. No eligible Work returns `no_work` without recording a request.

JSON output includes `status`, `request_id`, `snapshot_tip`, and metadata-only `work_ids`; authenticated publication additionally includes `publication`. Status is `dry_run`, `no_work`, `cancelled`, or `already_committed`. A preview does not guarantee later publication authority or eligibility. Inspect `explain --request-id ID` after an uncertain failure before retrying that same ID; a committed cancellation with pending Issue synchronization requires `sync-issues --request-id ID`, not another cancellation.

### Clear unfinished Work

```bash
Expand Down
9 changes: 9 additions & 0 deletions docs/src/content/docs/specs/work-queue-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,15 @@ request MUST recover its original request and fingerprint, not publish a checked
replacement. All existing authorization, ownership and native-reservation
requirements still apply to both forms.

Native `work cancel --obsolete` MUST use the maintenance eligibility predicate,
current-time expiration, UTF-8 byte ordering, and a single batch bounded by 256
and the installed operation limit. An optional smaller `--limit` only narrows
that bound. Publication uses the existing native administrator authority, not
an impersonated workflow reconciler. `--dry-run` MUST remain read-only and MUST
NOT assert authenticated publication authority. Retrying an accepted request
MUST recover its original targets and fingerprint rather than reselect a batch
from newly obsolete Work. No eligible targets MUST NOT record a request.

`WorkPriority` is published through an administrator `control` request in this
same transaction log. Replay stores `effective_priority` separately from the
immutable Work definition; native Go and JavaScript selection and historical
Expand Down
123 changes: 123 additions & 0 deletions pkg/cli/work_command_obsolete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cli

import (
"encoding/json"
"errors"
"fmt"
"time"

"github.com/github/gh-aw/pkg/workqueue"
"github.com/spf13/cobra"
)

const workObsoleteReason = "worker_contract_obsolete"

type workObsoleteResult struct {
Status string `json:"status"`
RequestID string `json:"request_id"`
SnapshotTip string `json:"snapshot_tip"`
WorkIDs []string `json:"work_ids"`
Publication *workqueue.Publication `json:"publication,omitempty"`
}

func workObsoleteIntent(state workqueue.Projection, at int64, limit int, id string) (workqueue.OperationsParameters, []string, error) {
ids, err := workqueue.ObsoleteWorkIDs(state, at, limit)
if err != nil {
return workqueue.OperationsParameters{}, nil, err
}
if accepted, ok := state.Requests[id]; ok {
params, err := workRecoverClearIntent(accepted, workObsoleteReason)
if err != nil {
return params, nil, err
}
if len(params.Operations) > min(limit, state.Policy.Limits.Operations) {
return params, nil, errors.New("obsolete_limit: original cancellation exceeds this batch limit; retry with its original limit")
}
ids = []string{}
for _, operation := range params.Operations {
var fields struct {
WorkID string `json:"work_id"`
ClaimID string `json:"claim_id"`
}
if err := json.Unmarshal(operation, &fields); err != nil {
return params, nil, err
}
if fields.ClaimID != "" {
return params, nil, errors.New("request_reuse: accepted cancellation selected Claims; retry its original operator command")
}
ids = append(ids, fields.WorkID)
}
return params, ids, nil
}
targets := make([]workOperatorTarget, 0, len(ids))
for _, id := range ids {
targets = append(targets, workOperatorTarget{WorkID: id})
}
params, _, err := workOperatorIntent(state, targets, workObsoleteReason, 0, id)
return params, ids, err
}

func workRunObsoleteCancellation(cmd *cobra.Command) error {
limit, _ := cmd.Flags().GetInt("limit")
if limit < 1 || limit > 256 {
return errors.New("obsolete_limit: --limit must be 1..256")
}
branch := workBranch(cmd)
commits, err := branch.Read(cmd.Context())
if err != nil {
return err
}
if len(commits) == 0 {
return errors.New("queue_missing: obsolete cancellation requires an installed queue")
}
at := time.Now().UnixMilli()
state, err := workqueue.ReplayAt(commits, at)
if err != nil {
return err
}
id, err := workRequestID(cmd)
if err != nil {
return err
}
params, ids, err := workObsoleteIntent(state, at, limit, id)
if err != nil {
return err
}
result := workObsoleteResult{Status: "no_work", RequestID: id, SnapshotTip: state.Tip, WorkIDs: ids}
message := "No eligible obsolete Work; no request recorded"
if len(ids) == 0 {
return workPrint(cmd, result, message)
}
dryRun, _ := cmd.Flags().GetBool("dry-run")
_, accepted := state.Requests[id]
if dryRun {
result.Status = "dry_run"
message = fmt.Sprintf("Would cancel %d obsolete Work; read-only selection, publication authority not checked", len(ids))
if accepted {
result.Status = "already_committed"
message = fmt.Sprintf("Original cancellation of %d Work already committed; read-only receipt recovery", len(ids))
}
} else {
return workPublishObsoleteCancellation(cmd, params, result)
}
return workPrint(cmd, result, message+"; native runs, reservations, policy, debt and history retained; request="+workQueueText(id))
}

func workPublishObsoleteCancellation(cmd *cobra.Command, params workqueue.OperationsParameters, result workObsoleteResult) error {
id := result.RequestID
published, err := workPublishOperator(cmd, id, "cancel_work", params)
if err != nil {
return fmt.Errorf("obsolete cancellation failed; inspect the ledger with explain --request-id %s before retrying the same request: %w", workQueueText(id), err)
}
result.Publication = &published
result.Status = "cancelled"
message := fmt.Sprintf("Cancelled %d obsolete Work", len(result.WorkIDs))
if !published.Changed {
result.Status = "already_committed"
message = fmt.Sprintf("Confirmed original cancellation of %d Work", len(result.WorkIDs))
}
if _, err := workBranch(cmd).SynchronizeIssueTransition(cmd.Context(), id); err != nil {
return fmt.Errorf("queue cancellation committed; Issue synchronization pending; retry sync-issues --request-id %s: %w", workQueueText(id), err)
}
return workPrint(cmd, result, message+"; native runs, reservations, policy, debt and history retained; request="+workQueueText(id))
}
Loading
Loading