Skip to content

Sync the product name to "OrcaCode Review", accept all four trigger spellings - #12

Merged
ZhenghuaBao merged 2 commits into
mainfrom
feat/orcacode-review-naming
Aug 18, 2026
Merged

Sync the product name to "OrcaCode Review", accept all four trigger spellings#12
ZhenghuaBao merged 2 commits into
mainfrom
feat/orcacode-review-naming

Conversation

@ZhenghuaBao

Copy link
Copy Markdown
Contributor

Two concerns, one release since they both touch action.yml.

Naming

The Action called itself Orca-Code-Review while the App brands its comments OrcaCode Review — one product signing the same PR two ways. This renames the display name only: action name, the brand default, the commit-status description, the summary-comment heading, README/NOTICE/recipe titles, and source header comments.

Deliberately not renamed, because each is an identity rather than a label:

Kept as-is Why
the four <!-- orca-code-review-* --> upsert markers The Action finds its own previous comments by these strings. Renaming them orphans every comment already posted in every consumer repo, and the next run posts a duplicate instead of editing in place.
<!-- orca-cr-summary:start/end --> Same, for the PR-description region.
the repo slug in uses:, and the .github/workflows/ filename Nobody reads a repo slug. The visible names above are what users see; changing the slug rewrites every consumer's workflow for zero benefit.

The summary heading is safe to change precisely because nothing matches on it — upsert goes through the marker, and the push counter through <!-- orca-cr-state: … -->. Verified: marker counts are byte-identical to main, and no line in the diff touches a marker or a uses:.

Trigger

The example workflow gated on /orca-code-review — a spelling the App deliberately dropped — so the command this repo documented did nothing on the App path.

Now accepts the full cross of both prefixes and both separators, matching the App exactly:

/orcacode-review    /orcacode review
@orcacode-review    @orcacode review

The cross is the point rather than a convenience. A partial set is a trap: the reader who writes the one spelling you left out gets no run, no comment, and no error to explain why.

Metering

This part was already staged in the working tree and is carried here rather than reverted, because it answers a question we currently cannot answer: what a review actually costs.

  • fact-proxy.mjs gains CR_USAGE_FILE per-call token accounting and CR_MAX_RPM. It keeps only a bounded tail of each response (usage is last in an OpenAI body) so SSE stays unbuffered, and every extraction and append is soft-fail — metering is observability and must never gate a review.
  • scripts/usage-summary.mjs turns that log into a per-model cost. It resolves the provider-echoed model name against the gateway's vendor-prefixed price keys, and refuses to guess when a basename maps to several vendors — a wrong ratio is worse than an honest gap. Models with no cache_ratio are billed at full input rate and flagged, because that is what the gateway does.
  • New inputs: concurrency (default 24 — the engine's own default of 8 was never set, which shows up as per-file timeouts on slower models), max-tools, meter.

Testing

  • 176 pass. The 5 failures in settings.test.mjs are a pre-existing libuv crash on Windows + Node 24 (UV_HANDLE_CLOSING, exit 0xC0000409) — identical on an unmodified tree, confirmed by stashing.
  • Both YAML files parse; name, brand default, and the expanded if verified after parse.
  • usage-summary.mjs smoke-tested against a hand-built JSONL: vendor-prefix resolution, snapshot-suffix stripping, missing-cache_ratio flagging, malformed-line skip with exit 0, and the arithmetic checked by hand.

Reviewer note

usage-summary.mjs ships without a test file, unlike every other script here. It is observability-only, never gates, and exits 0 on malformed input — but that is a real gap, not an argument that it doesn't need one.

Separately: brand is documented as "Name shown on PR comments", yet summary-comment.mjs hardcodes the heading instead of reading it. Pre-existing, left alone here to keep this a rename; worth a follow-up.

…pellings

Two concerns, one release since they both touch action.yml.

NAMING. The Action called itself "Orca-Code-Review" while the App brands
its comments "OrcaCode Review", so one product signed the same PR two
ways. Renames the display name only: action `name`, the `brand` default,
the commit-status description, the summary-comment heading, README/NOTICE
/recipe titles, and source header comments.

Deliberately NOT renamed, because each is an identity rather than a label:
  - the four `<!-- orca-code-review-* -->` upsert markers. The Action finds
    its own previous comments by these strings; renaming them orphans every
    comment already posted in every consumer repo and the next run posts a
    duplicate instead of editing in place.
  - `<!-- orca-cr-summary:start/end -->`, same reason for the PR-description
    region.
  - the repo slug in `uses:` and the `.github/workflows/` filename. Nobody
    reads a repo slug; the visible names above are what users see, and
    changing the slug would rewrite every consumer's workflow for nothing.
The heading is safe to change precisely because nothing matches on it —
upsert goes through the marker and the push counter through
`<!-- orca-cr-state: … -->`.

TRIGGER. The example workflow gated on `/orca-code-review`, a spelling the
App deliberately dropped, so the documented command did nothing on the App
path. Now accepts the full cross of both prefixes and both separators —
`/orcacode-review`, `/orcacode review`, `@orcacode-review`,
`@orcacode review` — matching the App exactly. The cross is the point: a
partial set is a trap, because the reader who writes the one spelling you
left out gets no run, no comment, and no error to explain it.

METERING (was already staged in the tree). `fact-proxy.mjs` gains
`CR_USAGE_FILE` per-call token accounting and `CR_MAX_RPM`, plus
`scripts/usage-summary.mjs` to turn that log into a per-model cost. It
keeps only a bounded tail of each response so SSE stays unbuffered, and
every extraction and append is soft-fail — metering is observability and
must never gate a review. New inputs: `concurrency` (default 24 — the
engine's own default of 8 was never set, which reads as per-file timeouts
on slower models), `max-tools`, and `meter`.

Tests: 176 pass. The 5 failures in settings.test.mjs are a pre-existing
libuv crash on Windows + Node 24, identical on an unmodified tree.
@ZhenghuaBao

Copy link
Copy Markdown
Contributor Author

@codex review

1 similar comment
@ZhenghuaBao

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8df4a6c52f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/fact-proxy.mjs Outdated
Comment on lines +415 to +416
upRes.on("end", () => {
recordUsage(tail.toString("utf8"), { usageFile, status, retries, seq: mySeq });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the resolved model outside the response tail

For a non-streaming response larger than 64 KiB, only this tail is passed to recordUsage, but OpenAI-shaped JSON places model near the beginning and usage at the end. This is reachable through the non-streaming judge call, which permits up to 32k output tokens; such calls retain their token counts but record model: null, so usage-summary.mjs groups them as (unknown) and cannot calculate their per-model cost. Capture the model from the prefix or while streaming rather than extracting both fields from the bounded tail.

Useful? React with 👍 / 👎.

Comment thread action.yml Outdated
Comment on lines +998 to +1003
# Token accounting for this run. Purely informational and soft-fail:
# `|| true` so a metering bug can never turn a completed review red.
if [ "$METER" = "true" ] && [ -s "$USAGE_FILE" ]; then
echo "::group::Token accounting"
node "$USAGE_SUMMARY" "$USAGE_FILE" || true
echo "::endgroup::"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Print token accounting when a review fails

When the mandatory engine pass times out, produces unusable output, or hits a policy block, run_review exits the shell at lines 918–946 before this block is reached. Those are exactly the costly failure cases that the proxy explicitly records, but the later always() cleanup deletes cr-usage.jsonl, so their accounting is never printed or recoverable. Move this soft-fail summary into an always() step (before cleanup) so metering covers failed runs as advertised.

Useful? React with 👍 / 👎.

Comment thread scripts/fact-proxy.mjs
Comment on lines +451 to +454
limiter.acquire().then(() => {
if (clientGone || res.destroyed) return;
dial(body, retries);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel queued limiter admissions for disconnected clients

With CR_MAX_RPM enabled, a request that disconnects while waiting remains in the limiter's serialized queue. When its turn eventually arrives, acquire() records a rate-limit slot before this callback notices clientGone, so the dead request sends nothing but still consumes capacity; a burst of timed-out per-file requests can consequently reserve successive minute windows and delay later live requests until the review's wall-clock timeout. Admission needs to be cancellable, or disconnected entries must be removed before they reserve a slot.

Useful? React with 👍 / 👎.

Comment thread scripts/fact-proxy.mjs
Comment on lines +408 to +412
upRes.on("data", (c) => {
const joined = tail.length ? Buffer.concat([tail, c]) : c;
tail =
joined.length > USAGE_TAIL_BYTES
? Buffer.from(joined.subarray(-USAGE_TAIL_BYTES))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode compressed responses before extracting usage

When the client advertises gzip or Brotli and the upstream honors it, these chunks contain compressed bytes rather than JSON because the proxy forwards accept-encoding and does not decompress the metering tap. The response still reaches OCR successfully, but every metering field is recorded as null or zero; this can affect ordinary HTTP clients that advertise compression by default. Either request identity encoding upstream or decode a separate copy for extraction while preserving the relayed bytes.

Useful? React with 👍 / 👎.

Round 1 of review follow-ups. Three of four findings were real; the fourth
is declined below with its reason.

1. `model` was read from the same bounded TAIL as `usage`, but an
   OpenAI-shaped body puts `model` near the START. Any non-streaming
   response larger than the tail therefore recorded `model: null`, and
   usage-summary groups those under "(unknown)" and cannot price them —
   which is the entire point of metering. Keep a small bounded head as
   well and prefer it for the model, falling back to the tail.

2. The proxy forwarded `accept-encoding` untouched, so a gzip or Brotli
   body reached the metering tap as compressed bytes and every token field
   came out null. This is the DEFAULT path, not an edge case: the engine is
   a Go binary and Go's net/http adds `Accept-Encoding: gzip` on its own.
   Ask upstream for identity while metering. Left alone when metering is
   off — nothing then justifies giving up compression.

3. Token accounting sat at the end of the review shell, which `exit 1`s on
   wall-clock timeout, unusable engine output, and policy blocks. Those are
   exactly the runs whose spend you want to see, since the tokens were
   spent either way, and the final cleanup deletes cr-usage.jsonl so the
   numbers were unrecoverable. Moved to its own `always()` step ahead of
   cleanup.

DECLINED — cancel queued limiter admissions for disconnected clients. The
mechanism is real: `acquire()` resolves and takes a slot before the callback
notices `clientGone`. But `createRateLimiter` returns a no-op acquire when
`maxRpm <= 0`, and action.yml never sets `CR_MAX_RPM`, so on the shipped
path this code cannot run. Making admission cancellable means restructuring
the limiter to buy nothing on any path we ship. Worth revisiting if and when
a rate ceiling is actually configured.

Tests: 3 added, and each was checked against a reverted fix — the two
fact-proxy tests fail without their fix and the third (accept-encoding
untouched when metering is off) passes either way by design.
fact-proxy.test.mjs is 38/38.
@ZhenghuaBao

Copy link
Copy Markdown
Contributor Author

Fixed 3 of 4 in 9fbab1f. One declined, with reasoning.

1. model outside the tail — fixed. Kept a bounded 4 KiB head alongside the 64 KiB tail and prefer it for model, falling back to the tail. You were right that this defeats the feature's purpose rather than merely degrading it: a (unknown) row is one usage-summary.mjs cannot price at all.

2. Compressed responses — fixed, and this was the worst of the four. accept-encoding is now forced to identity while metering. Worth noting the severity is higher than "can affect ordinary HTTP clients that advertise compression by default": the engine is an ordinary Go HTTP client, and Go's net/http adds Accept-Encoding: gzip on its own, so this was the default production path — metering would have silently recorded nothing. Left untouched when metering is off, since nothing then justifies giving up compression.

3. Token accounting on failed runs — fixed as you suggested: its own step with if: always(), placed before cleanup. Your reasoning was the deciding part — a wall-clock timeout is the most expensive outcome and the one whose token count was being discarded.

4. Cancel queued limiter admissions — declined. The mechanism is real; I confirmed acquire() resolves and takes a slot before the callback checks clientGone. But createRateLimiter returns a no-op acquire when maxRpm <= 0, and action.yml never sets CR_MAX_RPM — so on every path this Action ships, the code you describe cannot execute. Making admission cancellable means restructuring the limiter for a benefit that is currently unreachable. Recorded as worth doing if a rate ceiling is ever actually configured; happy to be pushed on this if you think a consumer is likely to set it directly.

Tests: 3 added, each validated by reverting its own fix — the two behavioural tests fail without the fix, and the third (accept-encoding untouched when metering is off) passes either way by design, which is the point. fact-proxy.test.mjs 38/38.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 9fbab1ff05

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ZhenghuaBao
ZhenghuaBao merged commit e6a65c4 into main Aug 18, 2026
@ZhenghuaBao
ZhenghuaBao deleted the feat/orcacode-review-naming branch August 18, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant