Skip to content

Port: throttle controls for the stop-time review gate #48

@JohnnyVicious

Description

@JohnnyVicious

Port of openai/codex-plugin-cc#20 (open)

Motivation

The stop-time review gate (plugins/opencode/scripts/stop-review-gate-hook.mjs) runs a targeted review on every Claude Code stop event when enabled. For a long, chatty session this can fire dozens of times, burning OpenCode tokens (and real money if the user configured a paid provider) faster than they realize.

There's a warning in the README, but there's no mechanism. Users have to either leave the gate off entirely or accept unbounded spend.

Proposed UX

# Allow at most 5 stop-gate reviews per session
/opencode:setup --review-gate-max 5

# Require at least 10 minutes between stop-gate reviews
/opencode:setup --review-gate-cooldown 10

# Combine both
/opencode:setup --enable-review-gate --review-gate-max 5 --review-gate-cooldown 10

# Remove a limit
/opencode:setup --review-gate-max off
/opencode:setup --review-gate-cooldown off

When a limit is reached, the stop-gate review is skipped (session is allowed to end) and a one-line note is logged. /opencode:review still runs manually.

Implementation

1. handleSetup in opencode-companion.mjs — accept two new value options:

async function handleSetup(argv) {
  const { options } = parseArgs(argv, {
    valueOptions: ["review-gate-max", "review-gate-cooldown"],
    booleanOptions: ["json", "enable-review-gate", "disable-review-gate"],
  });

  // ... existing setup body ...

  if (options["review-gate-max"] != null) {
    const max = options["review-gate-max"] === "off" ? null : Number(options["review-gate-max"]);
    if (max !== null && (!Number.isInteger(max) || max < 1)) {
      throw new Error(`--review-gate-max must be a positive integer or "off".`);
    }
    updateState(workspace, (state) => {
      state.config = state.config || {};
      state.config.reviewGateMaxPerSession = max;
    });
  }

  if (options["review-gate-cooldown"] != null) {
    const cooldown = options["review-gate-cooldown"] === "off" ? null : Number(options["review-gate-cooldown"]);
    if (cooldown !== null && (!Number.isInteger(cooldown) || cooldown < 1)) {
      throw new Error(`--review-gate-cooldown must be a positive integer (minutes) or "off".`);
    }
    updateState(workspace, (state) => {
      state.config = state.config || {};
      state.config.reviewGateCooldownMinutes = cooldown;
    });
  }
}

2. Persist per-session state in state.json:

{
  "config": {
    "reviewGate": true,
    "reviewGateMaxPerSession": 5,
    "reviewGateCooldownMinutes": 10
  },
  "reviewGateUsage": {
    "<sessionId>": { "count": 3, "lastRunAt": "2026-04-12T14:03:22Z" }
  }
}

3. Gate check in stop-review-gate-hook.mjs (add before the existing stdin read):

const sessionId = getClaudeSessionId();
const now = new Date();

if (sessionId && state.config) {
  const usage = state.reviewGateUsage?.[sessionId] ?? { count: 0, lastRunAt: null };

  const max = state.config.reviewGateMaxPerSession;
  if (Number.isFinite(max) && usage.count >= max) {
    console.log(`ALLOW: Review gate session cap (${max}) reached.`);
    return;
  }

  const cooldownMin = state.config.reviewGateCooldownMinutes;
  if (Number.isFinite(cooldownMin) && usage.lastRunAt) {
    const elapsedMs = now - new Date(usage.lastRunAt);
    if (elapsedMs < cooldownMin * 60 * 1000) {
      const remaining = Math.ceil((cooldownMin * 60 * 1000 - elapsedMs) / 1000);
      console.log(`ALLOW: Review gate cooldown (${remaining}s remaining).`);
      return;
    }
  }
}

// ...proceed with stdin read + OpenCode call...

// After a successful review, bump usage:
updateState(workspace, (s) => {
  s.reviewGateUsage = s.reviewGateUsage || {};
  const entry = s.reviewGateUsage[sessionId] ?? { count: 0, lastRunAt: null };
  entry.count += 1;
  entry.lastRunAt = now.toISOString();
  s.reviewGateUsage[sessionId] = entry;
});

4. Surface in /opencode:setup output so users can see their current limits and usage. In renderSetup:

Review gate: enabled (limit: 5/session, cooldown: 10 min)
  Used this session: 3/5, last run 4 minutes ago

5. Garbage-collect old session entries. On setup calls, drop entries older than 7 days so reviewGateUsage doesn't grow unbounded across many sessions.

Test plan

  1. Set reviewGateMaxPerSession: 2, simulate 3 stop events with the same session ID. Assert the third returns ALLOW: Review gate session cap (2) reached. without calling OpenCode.
  2. Set reviewGateCooldownMinutes: 10, run one stop event, wait simulated 5 min, run second. Assert second is skipped with cooldown message.
  3. Unset both limits — behavior matches current unbounded path.
  4. Different session IDs have independent counters.
  5. /opencode:setup with no flags surfaces current counts and limits.

Upstream reference

openai/codex-plugin-cc#20 (open).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions