Skip to content

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

Closed

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).

Activity

  1. added a commit that references this issue on Apr 12, 2026
    1c61980
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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