REQ-C-033: Commands Declare Interruption
Tier: Command Contract | Priority: P1
Source: §79 Work Outlives the Caller's Budget
Addresses: Severity: Critical / Token Spend: High / Time: Critical / Context: Medium
Description
A command whose run length the author cannot bound in advance (it grows with input size, remote load, or data shape) SHOULD declare interruption at registration, saying how its work survives the caller's sync call budget (REQ-F-080). The declaration has two independent properties:
detach: the work can continue in a background job after the call returns (REQ-F-081). It suits work that cannot be cut but can run unattended: a long HTTP call, a deploy, a buildresume: the command saves checkpoints, so the identical invocation continues the work after an interruption instead of starting over (REQ-C-035). It suits work made of chunks or steps, and it is the only protection when background processes do not survive (a CI container, a sandbox that kills orphans)
At least one of the two MUST be true. Work that is neither, such as a migration inside one transaction or an opaque third-party binary, declares no interruption and keeps the wall-clock limit of REQ-F-011 with an honest TIMEOUT (10) entry.
A command that declares interruption MUST:
- declare an
INCOMPLETE (14)entry inexit_codes:retryable: true, side_effects: "none"for read-only work,retryable: false, side_effects: "partial"for work that writes - report progress (REQ-C-034)
It MAY set idle_timeout_ms, how long its job may go without progress, and max_lifetime_ms, the bound on the whole work when the caller passes no --timeout.
The framework MUST refuse to register:
interruptiontogether withasync: true(REQ-C-022), which already returns at once; withstreaming_default: trueorsupports_streaming: true(REQ-O-004), whose output is a sequence of events with no single result to hand back later; or withstdout: "protocol"(REQ-C-032)resume: trueon amutatingordestructivecommand without--idempotency-key(REQ-C-007): a continuation of writing work must be recognisably the same run
Detaching by default is left out on purpose. It costs a worker process on every call, and a command reading stdin needs its input spooled first; one line of declaration opts in where it pays.
Acceptance Criteria
tool manifestshowsinterruptionon every command that declares it, and anINCOMPLETE (14)entry in itsexit_codes- Registering
interruption: {detach: false, resume: false}raises a framework error - Registering
interruptionwithout anINCOMPLETE (14)entry raises a framework error - Registering
interruptionwithasync: true, a streaming declaration, orstdout: "protocol"raises a framework error - Registering
resume: trueon amutatingcommand that does not accept--idempotency-keyraises a framework error - A command without
interruptionbehaves exactly as before: no--budgetflag, no handoff,TIMEOUT (10)at its wall-clock limit
Schema
Types: manifest-response.md · exit-code-entry.md
CommandEntry.interruption is {detach, resume, idle_timeout_ms?, max_lifetime_ms?}; the schema requires an exit_codes entry for "14" alongside it and excludes async: true, streaming_default: true, and stdout.
Wire Format
$ tool manifest
{
"schema_version": "3.20",
"framework_version": "2.4.0",
"etag": "sha256:91d4c7",
"commands": {
"analyze": {
"description": "Compute statistics over a data file",
"danger_level": "safe",
"required_scopes": [],
"interruption": { "detach": true, "resume": true },
"flags": {},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The statistics are computed", "retryable": false, "side_effects": "complete" },
"14": { "name": "INCOMPLETE", "description": "The work continues; run data.continue_command", "retryable": true, "side_effects": "none" }
}
},
"import": {
"description": "Import rows from a CSV file",
"danger_level": "mutating",
"required_scopes": ["rows:write"],
"interruption": { "detach": false, "resume": true, "max_lifetime_ms": 7200000 },
"flags": {
"idempotency-key": { "type": "string", "required": false, "description": "Key that makes a repeat of this import the same run" }
},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "Every row is imported", "retryable": false, "side_effects": "complete" },
"14": { "name": "INCOMPLETE", "description": "Some rows are imported; run data.continue_command", "retryable": false, "side_effects": "partial" }
}
}
}
}
Example
| Command | detach |
resume |
|---|---|---|
| Analysis of a large file | true |
true |
| One long HTTP call, a deploy | true |
false |
| Chunked import where background processes do not survive | false |
true |
| Migration inside one transaction | false |
false: declares no interruption and keeps TIMEOUT (10) |
register command "analyze":
danger_level: safe
interruption: { detach: true, resume: true }
exit_codes:
SUCCESS (0): retryable: false, side_effects: complete
INCOMPLETE(14): retryable: true, side_effects: none
Related
| Requirement | Tier | Relationship |
|---|---|---|
| REQ-F-080 | F | Provides: the declaration that subjects the command to the sync call budget |
| REQ-F-081 | F | Provides: detach, which hands the work to a background job |
| REQ-C-035 | C | Composes: resume promises the checkpoints that requirement defines |
| REQ-C-034 | C | Composes: every interruptible command reports progress |
| REQ-F-082 | F | Provides: the INCOMPLETE entry whose retryable the response copies |
| REQ-F-011 | F | Specializes: an undeclared command keeps the plain wall-clock limit |
| REQ-C-007 | C | Consumes: the key a resumable writer needs |
| REQ-C-022 | C | Composes: an async command already returns at once and declares no interruption |