paperclipai/paperclip · error · SemanticDispatchFailure

operation_unavailable

operation_unavailable

Error message

No mock operation is bound to this descriptor.

What it means

SemanticDispatchFailure with code `operation_unavailable`, thrown from the semantic tools dispatcher when an input descriptor maps to a command kind that has no case in #applyMutation's switch. The dispatcher only binds a fixed set of mock operations; descriptors outside that set fall into the default branch and are rejected, meaning the mock control plane cannot execute the requested operation.

Solutions

  1. Use one of the dispatcher's supported operations (e.g. control_workspace_service, schedule_wake) instead of the unbound descriptor
  2. Add a case for the new operation kind in #applyMutation of packages/paperclip-runner/src/semantic-tools/dispatcher.ts
  3. Verify the operationId/descriptor spelling matches a registered mock operation
  4. Check whether the mock fixture version supports the operation; upgrade the fixture if newer operations are needed

Example fix

// before (dispatcher.ts, #applyMutation)
case "control_workspace_service": ... break;
// default: throw new SemanticDispatchFailure("operation_unavailable", ...)
// after — add the missing binding
case "schedule_wake":
  command = { kind: "schedule_wake", taskId, reason: requiredString(input.reason), payload: optionalJson(input.payload), delayTicks: requiredNumber(input.delayTicks) };
  break;
Defensive patterns

Strategy: try-catch

Validate before calling

const SUPPORTED = new Set(["control_workspace_service", "schedule_wake" /* ...registered kinds */]);
if (!SUPPORTED.has(operation.kind)) {
  throw new Error(`Operation '${operation.kind}' is not bound in the mock dispatcher`);
}

Try / catch

try {
  const outcome = await dispatcher.execute(input);
} catch (e) {
  if (e instanceof SemanticDispatchFailure && e.code === "operation_unavailable") {
    // fall back to a supported operation or skip with a clear test failure
  } else throw e;
}

Prevention

When it happens

Trigger: #execute → #applyMutation with an operation whose normalized kind is not one of the handled cases (e.g. an unlisted or newly added semantic operation) so the switch hits `default`; calling a descriptor supported by the real control plane but not bound in the mock dispatcher.

Common situations: A new semantic operation was added upstream but the mock dispatcher switch was not extended; typo or version drift causing the descriptor to normalize to an unbound kind; tests exercising an operation the mock fixture does not implement.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/adf169330b52f8a5. Report an issue: GitHub.

Appendix: source

Thrown at packages/paperclip-runner/src/semantic-tools/dispatcher.ts:408

        };
        break;
      case "request_approval":
        command = { kind: "request_approval", taskId, approvalType: requiredString(input.approvalType), payload: optionalJson(input.payload) };
        break;
      case "decide_approval":
        command = { kind: "decide_approval", taskId, approvalId: requiredString(input.approvalId), decision: requiredString(input.decision) as Extract<CapabilitySemanticCommand, { kind: "decide_approval" }>["decision"], note: requiredString(input.note) };
        break;
      case "comment_on_approval":
        command = { kind: "comment_on_approval", taskId, approvalId: requiredString(input.approvalId), body: requiredString(input.body) };
        break;
      case "control_workspace_service":
        command = { kind: "control_workspace_service", taskId, serviceId: requiredString(input.serviceId), action: requiredString(input.action) as Extract<CapabilitySemanticCommand, { kind: "control_workspace_service" }>["action"], url: nullableOptionalString(input.url) };
        break;
      case "schedule_wake":
        command = { kind: "schedule_wake", taskId, reason: requiredString(input.reason) as Extract<CapabilitySemanticCommand, { kind: "schedule_wake" }>["reason"], payload: optionalJson(input.payload), delayTicks: requiredNumber(input.delayTicks) };
        break;
      default:
        throw new SemanticDispatchFailure("operation_unavailable", "No mock operation is bound to this descriptor.");
    }
    const outcome = await this.port.tryApplyCommand({ runId, idempotencyKey, command });
    if (operationId === "create_skill" && outcome.ok) {
      const id = outcome.result.entityRefs.find(ref => ref.startsWith("skill:"))?.slice(6);
      const skill = this.port.snapshot().skills?.find(candidate => candidate.id === id);
      if (skill) return readSuccess(outcome.result.stateRevision, {
        id: skill.id, name: skill.name, slug: skill.slug, description: skill.description,
        versionId: skill.versionId, studioPath: `/skills/studio/${skill.id}`,
      });
    }
    return commandOutcome(outcome);
  }

  #record(
    context: CapabilitySemanticPolicyContext,
    decision: CapabilitySemanticAuthorizationDecision,
    callId: string | null,
    input: unknown,

View on GitHub (pinned to 3f1d897a7c)