fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(eval): harden external prompt and admission handling - #2963

Open
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv
Open

fix(eval): harden external prompt and admission handling#2963
hqhq1025 wants to merge 5 commits into
apache:mainfrom
hqhq1025:codex/eval-external-prompt-argv

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes two Eval-side attribution failures found while completing the seven-arm Terminal-Bench matrix.

  • escapes dash-leading task instructions for positional external CLIs
  • passes ZCode prompts as one --prompt= argument and keeps its cwd task-derived
  • prefixes Pi input because Pi 0.84.1 treats dash-leading positional messages as options even after --
  • treats a successful 2xx response carrying a model request as inference admission when an external CLI uses an unrecognized provider event envelope
  • keeps non-2xx provider and transport failures classified as infrastructure failures

Real-host evidence:

  • pytorch-model-recovery previously failed before model admission across Codex, Claude Code, Reasonix, ZCode, and Pi because its instruction begins with -
  • corrected sparse reruns entered model execution and completed on Codex, Claude Code, Reasonix, and ZCode
  • Reasonix adaptive-rejection-sampler and gpt2-codegolf actually consumed model output but were misreported as pre-admission because the response envelope was not recognized
  • Reasonix client reasoning limits and task-native timeout failures remain third-party agent outcomes; this PR does not change their limits, prompts, effort, or timeouts

Validation:

  • Eval Node 31/31
  • relay contract 10/10
  • relay lifecycle 12/12
  • egress filter 3/3
  • run-trial policy 2/2
  • relay artifact 1/1
  • git diff --check

@hqhq1025hqhq1025 changed the title fix(eval): escape dash-leading task promptsfix(eval): harden external prompt and admission handlingAug 13, 2026
@hqhq1025
hqhq1025force-pushed the codex/eval-external-prompt-argv branch from a3dadfc to 82c22a0CompareAugust 13, 2026 13:17

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for chasing these down — the seven-arm matrix failing before model admission is a real problem and the diagnosis is sound. Two things block this one for me, both because they change numbers we publish rather than because the code is wrong in the abstract.

Before anything else: the description covers two of the five commits.5f920fa66 and 82c22a0f2 are described; 3dd99bb20, cfa20fe34 and eeec9529a are not, and they carry the largest behavior change in the PR — stream persistence, a usage checkpoint recovered through the relay, and a second place where admission decides scoring. The validation list almost certainly predates them. Whatever else changes, the body needs to describe the diff.

P0 — only Pi's prompt was modified. Laying the eight arms side by side:

armargs tail
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

Six arms receive context.taskInput verbatim. Pi receives it with a prefix. That is not a difference in argv syntax, it is a difference in the model's input — different first tokens, different framing of the instruction — and it lands in the spec that produces cross-arm comparison numbers. lifecycle-boundaries.test.ts:684 then freezes it as an assertion, so nobody downstream will read it as a defect.

P0 — admission now counts responses that never produced billable inference.origin/main had if (upstream.ok && parsed.admitted). This PR makes it parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model), which drops the upstream.ok guard from the first branch entirely — so the body's "keeps non-2xx provider and transport failures classified as infrastructure failures" no longer holds. A 500 carrying a choices array is now admitted. And isSuccessfulInferenceResponse itself only asks for a 2xx plus a model string somewhere in the request body, ignoring the URL path and the response content; a live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens — which claude-code issues routinely, which returns 200, and which bills nothing. Each of those flips an attempt from infra_failed (not scored, runner.ts:289) to failed (verifier runs, reward 0 recorded, runner.ts:296).

Root cause, and it is the same one twice. Both halves substitute a proxy signal for the fact actually needed. Admission needs "the provider produced billable output" — that fact lives in usageParser, and usageParser is what is broken: it splits on \n and JSON.parses each line, so a non-streaming, pretty-printed JSON body fails on every line and yields neither admission nor usage. The fallback makes the symptom disappear while the tokens and cost stay lost — a real billed pretty-JSON response still comes back usage: null, costUsd: null, usageComplete: false. Prompt delivery needs "every arm receives byte-identical task input", an invariant that is nowhere stated, so each arm's argv drifted on its own and one of them drifted into changing the prompt.

Minimal path. Restore the upstream.ok guard on the first branch. Narrow the fallback so it only applies when usage was parsed, which keeps the Reasonix cases while removing every row in that table. Give Pi the prompt unmodified — = form if it has such a flag, otherwise stdin (one change in the wrapper at harbor-external-subject.ts:429, currently stdio: ['ignore', 'pipe', 'pipe']), and failing both, drop Pi from the matrix rather than comparing it. Fix kimi-code's --prompt {{task.input}}, which is the exact shape you just declared broken for zcode. Replace the per-arm literal assertions with one parity assertion: after stripping flag syntax, every arm's prompt element equals {{task.input}}.

Cleanest path. Fix the parser instead of guarding around it: have finish() retry a whole-body JSON.parse when line-wise parsing produced nothing, which requires keeping the raw text rather than discarding buffered. That admits the Reasonix runs and recovers their usage and cost, and isSuccessfulInferenceResponse can be deleted rather than narrowed. Make prompt parity a spec-level invariant checked once for all arms instead of a literal per arm. And split this: the argv changes are self-evident and low risk, while the admission change rewrites scoring attribution and wants independent human review under AGENTS.md — right now it is spread across four commits and thirteen files, so it cannot be reverted on its own.

Two smaller notes. The new admission branch has no coverage — the integration children all send body: '{}' with no model field, so it never fires; reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green. And "escape" in the title is misleading: argv reaches the container through shlex.join plus shlex.quote (relay_agent.py:296, :305) and is spawned as an execve argv array, so no shell is involved and there is no injection surface — what these changes do is disambiguate each CLI's own option parser.


Review assistance: Claude Code (Opus) ran parallel passes over prompt delivery, admission attribution, and test quality, reproduced the misclassification table against a live metering proxy, and drafted this comment. I verified independently the parts it turns on: the eight-arm args table by decoding the spec, the upstream.ok deletion against origin/main, the five commits against the description, and the second admission consumer in external-subject.ts. The live reproduction rows and the mutation results are AI output that I have not re-run myself.

中文

先谢谢你把这些追出来——七臂矩阵在模型 admission 之前就失败是真问题,诊断也站得住。有两条我认为阻塞合并,原因不是代码抽象层面写错了,而是它们改的是我们会对外发布的数字。

先说一件事:描述只覆盖了五个提交里的两个。5f920fa6682c22a0f2 写了;3dd99bb20cfa20fe34eeec9529a 一个字没有,而它们承载了本 PR 最大的行为变更——流持久化、经 relay 回收的 usage checkpoint,以及第二处由 admission 决定计分的地方。Validation 里的数字几乎可以肯定早于这三个提交。无论其他怎么改,body 都需要如实描述这个 diff。

P0 —— 只有 Pi 的 prompt 被改了。 八臂并排看:

armargs 末尾
codex, claude-code, reasonix, opencode-- {{task.input}}
zcode--prompt={{task.input}}
kimi-code--prompt {{task.input}}
piTask instruction:\n{{task.input}}

六臂拿到的是逐字的 context.taskInput,Pi 拿到的多了一段前缀。这不是 argv 语法差异,是模型输入的差异——首 token 不同、指令的框架化程度不同——而且它落在产出跨臂对比数字的那份 spec 里。lifecycle-boundaries.test.ts:684 又把它冻结成断言,之后不会再有人把它读成缺陷。

P0 —— admission 现在会把从未产生计费推理的响应算进去。origin/mainif (upstream.ok && parsed.admitted),本 PR 改成 parsed.admitted || isSuccessfulInferenceResponse(upstream.status, projected.model),第一支的 upstream.ok 守卫被整个去掉了——所以正文里「keeps non-2xx provider and transport failures classified as infrastructure failures」不再成立,一个带 choices 的 500 现在算 admitted。而 isSuccessfulInferenceResponse 本身只要求 2xx 加上请求体里任意位置有个 model 字符串,不看 URL path、不看响应内容;真实起代理跑下来,200 包错误、200 空 body、204 No Content、以及 /v1/messages/count_tokens(claude-code 常规发送、返回 200、不计费)全部被判 admitted。每一条都会把 attempt 从 infra_failed(不计分,runner.ts:289)翻成 failed(跑 verifier、把 reward 0 写进 attempt,runner.ts:296)。

根因,而且是同一个根因出现了两次。 两半都用一个代理信号替换了真正需要的事实。admission 需要的事实是「provider 确实产出了可计费输出」——这个事实在 usageParser 里,而坏掉的正是 usageParser:它按 \n 切行、逐行 JSON.parse,于是非流式、被格式化过的 JSON 响应体每一行都失败,既拿不到 admission 也拿不到 usage。兜底让症状消失,token 和成本却仍然全丢——一次真实计费的 pretty JSON 响应回来仍是 usage: null, costUsd: null, usageComplete: false。prompt 传递需要的事实是「每一臂收到逐字节相同的任务输入」,而这条不变量没有被写在任何地方,于是各臂的 argv 各自漂移,其中一臂漂到了改 prompt 上。

最小方案。 恢复第一支的 upstream.ok 守卫。把兜底收窄成「仅当 usage 已解析成功时生效」,这样既保住 Reasonix 那两例,又消掉上表每一行。让 Pi 拿到未经修饰的 prompt——有 = 形式的 flag 就用,没有就走 stdin(改一处 wrapper,harbor-external-subject.ts:429 现在是 stdio: ['ignore', 'pipe', 'pipe']);两条都不行,就把 Pi 从矩阵里摘出来而不是拿它对比。修掉 kimi-code 的 --prompt {{task.input}},那正是你刚判定 zcode 要修的形态。把逐臂冻结字面量换成一条 parity 断言:剥掉 flag 语法后,每一臂的 prompt 元素都等于 {{task.input}}

最彻底方案。 修解析器而不是在它外面加护栏:让 finish() 在逐行解析没有产出时,对累积的完整 body 再整体 JSON.parse 一次(需要保留原始文本,目前 buffered 被丢弃)。这样 Reasonix 那几次既能正确 admitted,usage 和成本也一起修好,isSuccessfulInferenceResponse 可以直接删掉而不是收窄。把 prompt parity 做成 spec 级不变量、对所有臂检查一次,而不是每臂一条字面量。另外这个 PR 该拆:argv 那部分自证、风险低,而 admission 改的是计分归因,按 AGENTS.md 需要独立人审——它现在摊在四个提交、十三个文件里,没法单独 revert。

两条小的。新的 admission 分支没有覆盖——集成测试的 child 全部发 body: '{}'、没有 model 字段,所以它从未触发;把 harbor-external-subject.ts:762-763 还原成 main 那一行,provider-admission-integration.test.js 仍是 4/4 全绿。另外标题里的 "escape" 有误导:argv 经 shlex.join + shlex.quoterelay_agent.py:296:305)进容器,再以 execve argv 数组 spawn,全程没有 shell,不存在注入面——这些改动做的是消解各 CLI 自己的选项解析歧义。


评审协助说明:Claude Code (Opus) 并行跑了 prompt 传递、admission 归因与测试质量三轮审查,用真实 metering proxy 复现了那张误判表,并起草了本条评论。结论所依赖的部分我独立核实过:解码 spec 得到的八臂 args 表、对照 origin/main 确认 upstream.ok 被删、五个提交与描述的对照,以及 external-subject.ts 里第二处 admission 消费点。表中的实时复现行与变异测试结果属于 AI 输出,我没有自己重跑。

"--thinking",
"max",
"{{task.input}}"
"Task instruction:\n{{task.input}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — this is the only arm whose prompt content changed. The other six pass {{task.input}} verbatim; this one prepends Task instruction:\n, which changes the model's first tokens and how framed the instruction is. This file is the source of the cross-arm comparison, so Pi's column stops being comparable with the rest.

lifecycle-boundaries.test.ts:684 freezes it as assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}'), so from here it reads as contract rather than defect.

If Pi really does treat dash-leading positionals as options even after --, the options that preserve the prompt are a =-form flag (as zcode now uses), or stdin — the wrapper spawns with stdio: ['ignore', 'pipe', 'pipe'] at harbor-external-subject.ts:429, so that is one central change rather than a third per-arm workaround. If neither works, dropping Pi from the matrix is more honest than comparing it.

P2, same filekimi-code at :237-238 is still "--prompt", "{{task.input}}", byte-identical to the shape you just declared broken for zcode and fixed at :260. It is not in the failing-arm list because the eighth arm was not run; enabling it fails the same way. One line.

} finally {
const parsed = parser.finish();
if (upstream.ok && parsed.admitted) {
const admitted =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 — two things, one line.

origin/main:660 was if (upstream.ok && parsed.admitted). The new expression drops upstream.ok from the parsed.admitted branch, so a non-2xx response whose body happens to carry a recognized envelope is now admitted — isInferenceAdmissionEvent matches on Array.isArray(event.choices) alone, so a 500 with a choices body qualifies. That directly contradicts "keeps non-2xx provider and transport failures classified as infrastructure failures".

And isSuccessfulInferenceResponse asks only for 2xx plus a model string in the request body — no URL path, no response content. A live proxy run admits 200-wrapping-an-error, 200-with-empty-body, 204 No Content, and /v1/messages/count_tokens, which claude-code issues routinely and which bills nothing. Each flips infra_failed (not scored) to failed (reward 0 recorded).

There is also a quieter cost: usageComplete requires usageRequests === admittedRequests (:705, :792), so every newly-admitted request with no parseable usage pushes usageComplete false and drops costUsd to null at :101.

The narrow fix is (upstream.ok && parsed.admitted) || isSuccessfulInferenceResponse(...) plus requiring parsed usage before the fallback applies. The fix I would rather see is below at usageParser — the Reasonix cases are a parse gap, not an admission gap.

}
}

function usageParser(anthropic: boolean): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — this is where the Reasonix cases actually fail. Splitting on \n and JSON.parse-ing each line only works for SSE and JSONL; a non-streaming, pretty-printed JSON body fails on every line, so the request yields neither admission nor usage.

The fallback added above makes the admission half disappear while the usage half stays broken — a genuinely billed pretty-JSON response still returns usage: null, costUsd: null, usageComplete: false. Retrying a whole-body JSON.parse in finish() when line-wise parsing produced nothing fixes both, and lets isSuccessfulInferenceResponse be deleted rather than narrowed. It needs the raw text kept rather than buffered being discarded line by line.

Worth attaching the two Reasonix responses to the PR — without the actual envelopes there is no way to tell whether widening admission is even the right shape for them.

status: context.signal?.aborted
? ('indeterminate' as const)
: ('infra_failed' as const),
: recovered && recovered.admittedRequests > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — a second place where admission decides scoring, and it inherits everything above: any of the misclassified 2xx cases makes admittedRequests > 0 here and turns a result-transport failure into a scored failed.

Test coverage does not reach it. The new external-subject.test.ts case loops two executions, but the first has termination: 'framework_timeout' and returns at :89 before ever reaching this branch — both paths happen to produce 'failed', so the assertion passes while only testing one. Mutating :137-139 to a constant 'failed' leaves that file 8/8 green. A case with admittedRequests: 0 expecting infra_failed would close it.

return false;
}

export function isSuccessfulInferenceResponse(status: number, model: string | undefined): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2provider-admission.test.ts:24 asserts 204 is admitted. 204 No Content means the provider returned no output at all, so this writes a wrong semantic into the contract; it should be false, and the predicate narrowed to 200.

The rest of that test restates the two-line implementation — a range check and a non-empty string check — rather than exercising a behavior. The coverage that is missing is at the integration level: every child there sends body: '{}' with no model field, so this predicate never returns true in any integration case, and reverting harbor-external-subject.ts:762-763 to main's line leaves provider-admission-integration.test.js 4/4 green.

}
assert.equal(zcode.config.args?.includes('{{task.cwd}}'), true);
assert.equal(zcode.config.args?.at(-1), '--prompt={{task.input}}');
assert.equal(pi.config.args?.at(-1), 'Task instruction:\n{{task.input}}');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — freezing each arm's literal separately is what let Pi's prompt drift without anyone noticing; the assertions record what each arm does rather than what all arms must share.

One parity assertion would carry the invariant instead: strip the flag syntax (-- separator, --flag= prefix) and require every arm's prompt element to equal {{task.input}}. That fails loudly on the next arm that improvises, and it is the natural place to state the rule — nothing in the repo currently says the task input must reach every subject unmodified.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-machine validation update (VMSS, Terminal-Bench 2.1):

  • Fixed external CLI trajectory loss by streaming stdout/stderr to bounded /logs/agent/<profile>.jsonl and .stderr.txt files (64 MiB cap with observed/persisted/truncated bytes and SHA-256).
  • Added atomic per-request provider usage checkpoints at /logs/agent/<profile>.provider-usage.json. Framework timeout/result-frame loss now recovers already-settled usage/cost without inventing usage for an in-flight request.
  • Moved collected artifact inventory to executor finalization, so completed, subject-failed, infra-failed, and indeterminate attempts all record collected file bytes and SHA-256.
  • Preserved native task timeout and verifier execution after timeout when model admission/settled evidence exists.

Validation:

  • Eval Node 33/33; relay contract 10/10; lifecycle 12/12; egress 3/3; policy 2/2; artifact 2/2.
  • OpenCode canary: score=1, usage=32,908 tokens; JSONL/checkpoint/stderr/wrapper-state hashes matched collected artifacts.
  • Gracefully cancelled infra canary: indeterminate attempt still retained all four collected artifact hashes.
  • Claude model-extraction-relu-logits: native 900s agent timeout, verifier score=1, recovered 94,225 tokens, 8.9 MB JSONL, all hashes recorded.
  • Claude raman-fitting: score=0, recovered 2,777,464 tokens.
  • Reasonix make-doom-for-mips: score=0, recovered 3,963,729 tokens.

Remaining non-Eval gaps reproduced with complete evidence: Reasonix adaptive/gpt2 and Kimi adaptive fail before model admission, so no provider usage exists to recover.

Validated deployment commit: eeec9529a.

@Astro-Han

Copy link
Copy Markdown
Contributor

Writing as a downstream consumer of admittedRequests rather than as a reviewer of the fix — #2971 adds a DeepSeek Harness arm that reads this field, so the widening in harbor-external-subject.ts:762-763 lands on that arm directly.

The problem being solved is real. The fact you want is "the provider produced billable output", and that fact currently lives in usageParser, which is the part that is broken. Reaching for a proxy signal is a reasonable response to that. What I want to flag is where this particular proxy signal lands, because I do not think the blast radius is visible from the diff.

main requires both conditions:

if(upstream.ok&&parsed.admitted)

The PR requires either of two, neither of which is that:

constadmitted=parsed.admitted||isSuccessfulInferenceResponse(upstream.status,projected.model)

Two separate widenings. parsed.admitted no longer requires upstream.ok, so a non-2xx response whose stream happened to carry an admission event now counts. And isSuccessfulInferenceResponse is 2xx && typeof model === 'string' && model.length > 0 (provider-admission.ts:22), so a 2xx that produced no usage at all counts too.

Three consumers in my arm, and two of them decide whether an attempt is retried.

  1. classifyExecution (harbor-external-subject.ts:621) returns infra_failed when admittedRequests === 0.
  2. The interrupted-settlement recovery paths (external-subject.ts:145,175,206,211) choose failed over infra_failed on recovered.admittedRequests > 0.
  3. usageComplete is inFlightRequests === 0 && admittedRequests > 0 && usageRequests === admittedRequests (harbor-external-subject.ts:707).

The first two are the same boundary, and that boundary is the difference between an infrastructure retry and a recorded result. Widening admission moves an attempt from "retry it" to "the subject failed" — which scores zero and does not retry. A provider 500 whose stream carried an admission event, or a 2xx that yielded no usage, becomes a silent zero rather than a retried cell.

The third one moves the other way: admittedRequests grows while usageRequests does not, so usageRequests === admittedRequests fails more often, usageComplete goes false and costUsd goes null. Cost attribution gets sparser exactly on the requests this change was meant to account for.

The part I most want on the record: in the head-to-head cohort this is one-sided.experiments/terminal-bench-2.1-deepseek-v4-flash-maka-vs-deepseek-harness.json pairs Maka against the harness arm in one task group. The Maka arm is kind: maka and runs through harbor-maka-subject.js — it never touches this metering proxy. So a change to admission semantics shifts the failure classification of one arm and not the other. A classification shift that only moves one arm is the specific thing a paired comparison cannot absorb.

I am not asking for a particular fix, and I do not think the answer is to revert to upstream.ok && parsed.admitted if that is what is dropping real usage. Two things that would resolve it for me:

  • Keep upstream.ok as a conjunct. The second widening is arguably defensible on its own; dropping the status check is a separate change that I do not think the described problem requires.
  • If admission has to widen, separate "admitted" from "billable" so usageComplete and the retry boundary do not both move on one signal. My arm needs the retry boundary to stay conservative far more than it needs admission to be generous.

Happy to adapt on my side instead if you would rather the semantics change and the consumers follow — but then I would want to land that after #2971, so the arm is not written against a definition that is mid-flight.

Separately, and unrelated to the above: this PR and #2971 both read /logs/agent/<profile>.provider-usage.json under schema maka.external_provider_usage.v1, yours from the relay before teardown, mine host-side from the trial directory. Those look complementary rather than competing, but the schema needs one owner. I have deferred adding a field to it (provider finish reasons) specifically because this PR is in flight.

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)📘 Rule violations (0)📜 Skill insights (0)

Grey Divider


Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security
Description
Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted this exact subject-controlled checkpoint trust-boundary bug and required
authentication or host-owned isolation.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper writes the checkpoint to a fixed path in the external process's log directory before
runChild, while the relay reads, parses, and forwards any dictionary found there only after the
child terminates. The measured process can therefore replace the checkpoint before recovery, and
although structural schema and path checks validate its shape and name, they do not establish its
provenance; the adapter nevertheless trusts the recovered admission count, usage, and cost for
accounting and outcome classification.

packages/eval/src/harbor-external-subject.ts[53-57]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/external-subject.ts[130-141]
packages/eval/src/harbor-external-subject.ts[55-57]
packages/eval/src/harbor-external-subject.ts[696-712]
packages/eval/src/external-subject.ts[130-140]
packages/eval/src/external-subject.ts[195-235]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability
Description
Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Code

packages/eval/harbor/relay_agent.py[R335-336]

+ await environment.download_file(source, destination)+ value = json.loads(destination.read_text(encoding="utf-8"))
Relevance

●●● Strong

PR 3176 accepted the matching oversized-checkpoint risk and required bounded regular-file reads with
a 64 KiB limit.

PR-#3176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The recovery source resides in the subject environment, and the only new validation is a pathname
regex. After the initial checkpoint is written, the wrapper’s child can modify that pathname; the
relay then transfers the complete source file to a host temporary directory, reads the entire file
as UTF-8 text, and parses it with json.loads, with no stat/type check or byte cap anywhere on this
path. Prior accepted work in the same area explicitly required bounded regular-file reads for
subject-replaceable checkpoints.

packages/eval/harbor/relay_agent.py[266-271]
packages/eval/harbor/relay_agent.py[324-339]
packages/eval/src/harbor-external-subject.ts[77-96]
packages/eval/src/external-subject.ts[82-88]
PR-#3176

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness
Description
Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Code

packages/eval/src/external-subject.ts[R227-230]

+ costUsd:+ checkpoint.costUsd === null+ ? null+ : nonnegative(checkpoint.costUsd, 'external checkpoint cost'),
Relevance

●●● Strong

PR 2915 explicitly accepted the same cost-accounting rule: publish cost only when every admitted
model has usage.

PR-#2915

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Checkpoint writing computes a cost whenever any usage has been measured, independently of
completeness. The decoder checks only that usageComplete is boolean and returns that cost, and
both recovery branches consume it. In contrast, the normal completion path requires
usageComplete() before reporting cost.

packages/eval/src/harbor-external-subject.ts[99-102]
packages/eval/src/harbor-external-subject.ts[697-705]
packages/eval/src/external-subject.ts[218-235]
packages/eval/src/external-subject.ts[89-96]
packages/eval/src/external-subject.ts[130-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR adds substantial, independently risky logic across external CLI argument handling, provider-admission classification, relay recovery/artifact persistence, stream truncation, and lifecycle finalization, making multiple subtle defects plausible to miss in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Trusts subject usage checkpoint 🐞 Bug⛨ Security

Fix-now: the relay accepts a schema-valid checkpoint from the subject-controlled /logs/agent path,
which the external CLI can overwrite before exit, and the adapter trusts its admittedRequests,
usage, and cost to change a missing-result-frame outcome from infra_failed to failed. A
task-controlled agent can therefore forge model-admission and billing evidence, violating the
attribution boundary the recovery path is intended to preserve.
Agent Prompt
## Issue description
The recovery path accepts a schema-valid checkpoint from a subject-controlled location without proving that the metering proxy produced it. Do not trust this file as accounting or admission evidence without host-owned integrity protection, because a forged checkpoint can alter reported usage, cost, and inference-admission classification.
## Issue Context
The wrapper creates the checkpoint before launching the external CLI, and the relay downloads it after that CLI exits, allowing the same external process to replace the file in between. Path validation and structural schema checks constrain only the checkpoint's name and shape, not its provenance or contents.
Reuse the existing relay result token as the checkpoint-authentication secret; it is already shared between the wrapper and relay, avoiding another secret, configuration field, or authority. Have the wrapper MAC a canonical checkpoint payload, and make the relay reject recovery checkpoints whose MAC is absent or invalid before forwarding their data.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/src/external-subject.ts[195-235]
- packages/eval/src/harbor-external-subject.ts[695-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +335 to +336
await environment.download_file(source, destination)
value = json.loads(destination.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bounds recovery checkpoint reads 🐞 Bug☼ Reliability

Fix-now: _read_recovery downloads, decodes, and JSON-parses an arbitrarily large
subject-controlled file selected from the writable log directory without a size or regular-file
check. A child can replace the checkpoint before timeout, teardown, or result-frame recovery,
causing unbounded host temporary-disk, memory, and JSON-parsing work; the fixed pathname regex does
not constrain the target’s type or size.
Agent Prompt
## Issue description
Recovery downloads and parses an arbitrarily large subject-controlled checkpoint, allowing host temporary-disk, memory, and JSON-parsing exhaustion during timeout, teardown, or result-transport failure handling. Reject missing, non-regular, symlinked, and oversized checkpoint data before decoding JSON.
## Issue Context
The path regex protects only the pathname; it does not constrain the type or size of the file the external process leaves at that path. The existing result-carrier limit cannot be reused because it applies only after stdout is already present in relay memory, while this recovery path uses `environment.download_file`. Introduce a small dedicated checkpoint byte limit, reject non-regular, symlinked, or oversized sources before transfer where the environment API permits, and enforce the same limit while reading the downloaded file so resource exhaustion cannot prevent finalization.
## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[33-38]
- packages/eval/harbor/relay_agent.py[324-339]
- packages/eval/harbor/test_relay_artifacts.py[67-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +227 to +230
costUsd:
checkpoint.costUsd === null
? null
: nonnegative(checkpoint.costUsd, 'external checkpoint cost'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Reports incomplete checkpoint costs 🐞 Bug≡ Correctness

Fix-now: decodeUsageCheckpoint accepts usageComplete: false but still returns costUsd, so
timeout and result-frame-failure paths report an estimate made from only a subset of admitted
requests. This contradicts the wrapper’s normal-result contract, which deliberately returns
costUsd: null until all admitted requests have usage.
Agent Prompt
## Issue description
Do not expose checkpoint cost when its `usageComplete` flag is false. Recovered partial usage may remain useful, but its estimated cost is not authoritative.
## Issue Context
The normal wrapper result already suppresses cost unless all admitted requests have usage. Recovery should preserve that same accounting invariant rather than interpreting the checkpoint estimate differently.
## Fix Focus Areas
- packages/eval/src/external-subject.ts[218-235]
- packages/eval/src/harbor-external-subject.ts[99-102]
- packages/eval/src/harbor-external-subject.ts[697-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jackwenerjackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Automated review of exact head eeec9529ad69dd0be90408f8e35deaaea871181e against current main@d62857a8357e9160926726a2a13096bc2dc2b91d.

The reported failures still exist on current main: external arguments interpolate dash-leading task text without a profile-aware option boundary, and admission still depends only on recognized response envelopes. The repair direction is therefore relevant, but this revision is not safe to transplant as written.

P1 — recovered metering is subject-forgeable and unbounded. The wrapper writes ${profile}.provider-usage.json inside /logs/agent, the same writable environment in which the measured external CLI runs. After the child exits, relay_agent.py:_read_recovery downloads and JSON-parses that path with no authentication, regular-file proof, or byte cap, and external-subject.ts:decodeUsageCheckpoint trusts its usage, cost, and admittedRequests to decide billing and whether a missing result is an infrastructure retry or a scored subject failure. A task-controlled process can replace the file, forge a non-retryable zero/result, or exhaust host disk/memory during download. Current main already has the correct single authority: signed, bounded, atomic metering-checkpoint data keyed by meteringSecret; rebasing must preserve it rather than reintroduce this older plain-JSON protocol.

P1 — non-2xx responses can become admitted attempts.harbor-external-subject.ts:762-764 uses parsed.admitted || isSuccessfulInferenceResponse(...); the first branch no longer requires upstream.ok. An error response carrying a recognizable event therefore increments admittedRequests, moves recovery from infra_failed to failed, suppresses retry, and can record a zero. Keep success status as a conjunct for the retry boundary; if “billable” and “admitted” need different widening, represent them as different facts instead of sharing one counter.

Required conclusions:

  1. Optimal for the actual problem: no; useful prompt/artifact changes are coupled to a superseded metering authority.
  2. Production code to delete: delete the PR's duplicate plain-JSON checkpoint/recovery path after rebasing; reuse current main's signed checkpoint.
  3. Tests to delete/replace: replace fixture-written trusted checkpoint tests with adversarial forged/oversized/non-regular checkpoint cases against the real signed seam.
  4. Deeper refactor: rebase first, preserve the current metering authority, then transplant the remaining prompt escaping, artifact inventory, and narrowly defined admission change.
  5. Ready to merge: no; it is also currently conflicting with main.
  6. Residual risks/gaps: Eval scoring, retry classification, and usage/cost are protected contract behavior and require independent human review plus exact-head CI and real-host evidence after rebase.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/eval/src/harbor-external-subject.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NO-GO at exact head eeec9529ad69dd0be90408f8e35deaaea871181e. One integration P1 is inline. Multiple existing, non-outdated unresolved threads already cover the prompt-parity, admission, parser, retry-classification, and recovered-metering defects on this head; the existing CHANGES_REQUESTED review remains current. The branch is 334 commits behind current main. A real five-commit rebase probe conflicts in packages/eval/src/harbor-external-subject.ts; conflict resolution must retain current main's signed, bounded v2 metering/checkpoint authority and transplant only orthogonal fixes. The old exact-head CI is green, but it predates and does not validate that integration.

models: [...requestModels].sort(),
toolNames: [...observedToolNames].sort(),
};
checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(checkpointPath, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Resolve the rebase conflict by deleting this unsigned v1 checkpoint authority. This snapshot is written without a MAC and the PR's recovery path later trusts it from subject-writable /logs/agent, so choosing or blending this side would let the measured process forge usage, cost, and admittedRequests. Current main already has the canonical replacement: signMeteringCheckpoint(..., resultToken), a bounded O_NOFOLLOW regular-file read, v2 settlement/in-flight fields, and shared derivation of completeness/cost. Preserve that entire authority and transplant only the independent prompt/parser/artifact changes; do not revive this parallel schema or its fixture-trusted tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — one additional correctness reason to preserve the current-main checkpoint authority here: this branch chains checkpointWrites = checkpointWrites.then(() => writeJsonAtomic(...)) without containing a single write rejection. After an ENOSPC, EACCES, or rename failure, every later persistCheckpoint() stays rejected; both proxy.settle() and close() await that poisoned chain, so a provider request that already completed successfully is ultimately reported as infra_failed because an auxiliary recovery snapshot could not be refreshed. Current main deliberately catches each atomic-write failure and keeps the previous truthful lower-bound snapshot. Please preserve that behavior during the rebase as well. If checkpoint persistence is intended to be a hard run gate rather than best-effort recovery evidence, please push back and clarify that invariant.

中文摘要

再补充一个保留 current-main checkpoint authority 的正确性原因:这里一次原子写失败会永久污染 promise chain,随后让本已成功的运行在 settle/close 时误报 infra_failed。current main 明确把单次写失败收敛为保留旧的真实下界快照;重基时也请保留这个不变量。若 checkpoint 写入本来就是硬门禁,也欢迎直接说明。

AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hqhq1025@Astro-Han@jackwener