Action recovery
Octipus records mutating tool calls before executing them. If a call’s outcome cannot be established, later mutations in the same user/session require a separate recovery approval. This protects against silently repeating an action that may already have happened.
For example, a tool sends a message and its connection then fails. Octipus must not assume the message was never sent. A shell command that performs an action and subsequently times out has the same problem.
What the user sees
Section titled “What the user sees”The normal approval interface shows Check earlier
- Approving permits further mutations, including a retry if that is still the proposed action. It does not prove the previous action failed.
- Denying prevents that mutation and preserves the unresolved evidence.
- Read-only tools remain available for checking external state. Arbitrary shell commands are conservatively treated as mutations, even when the intended command would only inspect something.
- Normal permission denials still apply. Recovery approval cannot grant a tool permission revoked while the prompt was open.
- Unattended work cannot approve its own recovery. It reports that review is required.
The review covers specific records, in batches of up to 20. An action that becomes uncertain while the user is deciding needs its own review. Recovery approval does not become a permanent tool permission.
MCP server recovery declarations
Section titled “MCP server recovery declarations”The MCP server editor offers Trust server recovery declarations, off by
default (trustRecoveryHints: true in the server configuration). Enable it only
for a server whose implementation you trust to describe side effects correctly.
Like other server settings, it applies on the next connection.
For an opted-in server:
- An exact
annotations.readOnlyHint: trueon a discovered tool lets that call inspect state without a mutation journal or recovery review. It does not grant normal tool permission, and call arguments cannot set this annotation. - An error result may attest that the action never reached its mutation boundary:
{"isError":true,"_meta":{"octipus/recovery":{"outcome":"not_executed"}}}. The bridge converts this trusted evidence into the internal result identity. A server must never send this after an external write was dispatched.
Local bridge refusals before tools/call are also not_executed. Generic errors,
“refused” text, idempotentHint, missing results and post-dispatch transport
failures remain conservative. Recovery does not automatically retry any action
or change its retry limit. New MCP journal entries identify server.tool, rather
than only mcp_call_tool. Historical uncertain entries remain unchanged.
Pipeline resume and rewind
Section titled “Pipeline resume and rewind”Resuming or rewinding reruns work from a checkpoint. Even tools that returned successfully in an earlier run can be called again when their stage reruns. Octipus therefore requests Review pipeline replay before changing recovery checkpoints or starting workers when the pipeline has previous execution. The review conservatively covers the pipeline’s prior action records, rather than trying to infer every stage that a changed graph will revisit.
Older runs and vendor-native CLI tools may have no action journal records. Missing records do not establish that no action happened: the replay prompt explicitly warns about that uncertainty. A pipeline paused before any stage ran does not need this replay review.
Rejecting a replay preserves the checkpoints. Concurrent resume requests cannot start a second walker while the first request is awaiting review.
Implementation and guarantees
Section titled “Implementation and guarantees”The tool_actions table records an action ID, user/session/agent ownership,
pipeline/node correlation where present, tool identity, an argument hash,
timestamps, outcome and any recovery review ID. It does not store argument or
result bodies. Session or user deletion removes the associated journal records.
The execution boundary writes started before calling the tool. If that write
fails, the tool does not execute. A confirmed return is recorded as completed:
this means a finished attempt, not necessarily success. A normal shell exit,
including a failed test or an exit-code-2 directory lookup, is a completed attempt.
A shell parser refusal, policy rejection or proven failure before process startup
is not_executed; it does not require recovery approval before the next command.
A timeout, kill, signal, post-start cancellation or missing completion evidence is
uncertain, because partial effects may have occurred. Generic tool failures
(success: false, MCP isError, or thrown transport errors) remain conservative:
they can hide a remote action followed by a lost acknowledgement. Trusted
preflight evidence is carried by an internal error/result identity, not a flag a
model can supply. Permission revocation before the body starts is not_executed.
Historical uncertain records are not automatically cleared by these rules.
If the tool returns but its completion record cannot be saved, its known output
is preserved and the persisted started record blocks later mutations pending
review. An acknowledgement is separate from the outcome: reviewing an
uncertain action does not relabel it as completed.
BaseTool handlers record after middleware authorization and argument rewriting. Other external handlers dispatched through ToolExecutor are recorded there. Trusted read-only declarations bypass mutation recording. Internal orchestration and context tools remain available and do not create duplicate outer records around delegated work.
This assumes one active backend per database. Process-local tracking separates
currently running actions from orphaned started records after a restart.
Boundaries
Section titled “Boundaries”This is replay protection, not exactly-once delivery. It does not undo an external action, automatically inspect every vendor’s state, or inject a generic idempotency key into unrelated APIs. Use read-only checks and human review where an outcome cannot be determined. Provider-specific reconciliation and idempotency adapters can extend this without weakening the default gate.
A new session has a separate journal scope. Successful actions are not globally deduplicated: explicitly sending the same message again remains possible. Existing runs before this migration have no per-action evidence. Direct vendor CLI actions outside the Octipus tool bridge, standalone integrations that bypass these execution boundaries, and arbitrary effects inside plugin hooks are not individually journaled. Pipeline replay warns about work without records; that warning is not per-tool coverage of a vendor CLI.
Validation
Section titled “Validation”Regression tests cover:
- A real side effect followed by completion-write failure, database close/reopen, and denied retry without repeating the effect.
- A shell command that appends to a file and then times out.
- Parser refusals and ordinary failed shell exits allowing the next command without recovery approval, while interruption still requires review.
- Intent-write failure blocking execution, and one journal entry through the combined executor/BaseTool path.
- Read-only reconciliation, policy revocation during recovery approval, exact review scope, cross-user/session isolation and cancellation of queued calls.
- Denied rewind preserving checkpoints, concurrent resume exclusion, and stop during reconstruction preventing restarted workers.
- Embedded migrations, session deletion cleanup and external PostgreSQL journal persistence.
Validation for this change: 3,525 tests passed in the broad core, security, tools, connectors and pipeline API run (56 skipped); four selected external PostgreSQL tests passed. The broad run includes the embedded database reopen and real subprocess failure scenarios described above.
Run the focused checks with:
npm test -- src/core/action-recovery src/core/agent/pipeline-loop.test.tsnpm run test:integration -- src/db/repositories/tool-action-repository.test.ts src/core/agent/pipeline-interrupted.test.ts