fix: retry released probe locks and retain late-rejection repair context

This commit is contained in:
garrytan committed 2026-10-08 03:02:22 +00:00
1 parent 9e6d699598
commit b4ba71b28d
4 files changed
+49 -3

No files matched your search

+11 -2
View File
@@ -207,7 +207,9 @@ _gstack_codex_role_ready() {
_gstack_codex_auth_probe || return 1
_gstack_codex_sandbox_preflight || return $?
_gstack_codex_version_check
_gstack_codex_model_probe "$1"
_gstack_codex_model_probe "$1" || return $?
[ -z "${_GSTACK_CODEX_SEL_REPAIR:-}" ] || echo "HINT: If the review rejects the selected model, repair its winning source: $_GSTACK_CODEX_SEL_REPAIR."
return 0
}
# --- Sandbox (B1) -----------------------------------------------------------
@@ -595,7 +597,14 @@ _gstack_codex_lock_acquire() {
rmdir "$_dir" 2>/dev/null
return 2
fi
[ -d "$_dir" ] || return 2
if [ ! -d "$_dir" ]; then
{ [ -e "$_dir" ] || [ -L "$_dir" ]; } && return 2
{ [ -d "${_dir%/*}" ] && [ -w "${_dir%/*}" ] && [ -x "${_dir%/*}" ]; } || return 2
[ "$_i" -ge "$_tries" ] && return 2
_i=$((_i + 1))
sleep 0.1
continue
fi
_now=$(date +%s 2>/dev/null || echo 0)
_owner=$(cat "$_dir/owner" 2>/dev/null)
_dead=0
+24
View File
@@ -67,6 +67,30 @@ const withFixture = (body: (f: Fixture) => void | Promise<void>) => async () =>
};
describe('bounded multi-signature probe cache', () => {
test('a write-lock holder releasing after a failed mkdir does not drop a negative entry', withFixture((f) => {
const lock = path.join(f.locks, 'write');
const once = path.join(f.home, 'release-once');
fs.mkdirSync(lock, { recursive: true });
expect(fs.realpathSync(lock).startsWith(`${fs.realpathSync(f.home)}${path.sep}`)).toBe(true);
fs.writeFileSync(path.join(lock, 'owner'), `${process.pid} ${os.hostname()} ${Math.floor(Date.now() / 1000)}\n`);
fs.writeFileSync(once, 'armed');
fs.writeFileSync(path.join(f.home, 'bin/mkdir'), `#!/usr/bin/env bash
if [ "$#" -eq 1 ] && [ "$1" = "$RACE_LOCK" ] && [ -f "$RACE_ONCE" ]; then
rm -f "$RACE_ONCE" "$RACE_LOCK/owner"
rmdir "$RACE_LOCK"
exit 1
fi
exec ${JSON.stringify(Bun.which('mkdir'))} "$@"
`, { mode: 0o755 });
const result = probe(f, { ...model('gpt-race'), STUB_MODE: 'quota', RACE_LOCK: lock, RACE_ONCE: once });
expect(result.stdout).toContain('rc=4');
expect(fs.existsSync(f.cache)).toBe(true);
expect(entries(f)).toHaveLength(1);
expect(entries(f)[0]).toStartWith('MODEL_QUOTA_EXHAUSTED ');
expect(probe(f, model('gpt-race')).stdout).toContain('MODEL_QUOTA_EXHAUSTED (cached)');
expect(calls(f)).toBe(1);
}));
test('A -> B -> A makes exactly two paid probes', withFixture((f) => {
expect(probe(f, model('gpt-a')).stdout).toContain('MODEL_OK');
expect(probe(f, model('gpt-b')).stdout).toContain('MODEL_OK');
+13
View File
@@ -43,6 +43,7 @@ if (probe) {
process.exit(0);
}
if (!claude || args.includes('-')) await Bun.stdin.text();
if (!claude && process.env.FAKE_DISPATCH_MODEL_ERROR) { console.error('ERROR: The model is not supported by this account.'); process.exit(1); }
const response = process.env.FAKE_RESPONSE || 'Medium: the plan couples two rollouts.\\nRecommendation: split the migration because the plan couples two rollouts.';
if (claude && process.env.FAKE_PROVIDER_ERROR) { console.log(JSON.stringify({ is_error: true, result: process.env.FAKE_PROVIDER_ERROR })); process.exit(Number(process.env.FAKE_ERROR_EXIT ?? '1')); }
if (claude) console.log(JSON.stringify({ result: response, session_id: 's', modelUsage: { 'model-actual': { inputTokens: 1 } } }));
@@ -306,6 +307,18 @@ describe('Codex plan-review invocation binds one selection', () => {
expect(r.stdout).not.toContain('OUTSIDE_STATUS');
});
test('a dispatch-only Codex model rejection retains the selected-source repair after unverified readiness', () => {
const h = home();
fs.writeFileSync(path.join(h.state, '.codex-model-probe.locks'), 'not a directory');
const r = run('claude', h, { GSTACK_CODEX_MODEL: 'gpt-env', FAKE_DISPATCH_MODEL_ERROR: '1' });
expect(r.status).toBe(1);
expect(r.calls.map((call: { probe: boolean }) => call.probe)).toEqual([false]);
expect(r.stderr).toContain('MODEL_PROBE_INCONCLUSIVE');
expect(r.stderr).toContain('If the review rejects the selected model');
expect(r.stderr).toContain('unset GSTACK_CODEX_MODEL');
expect(r.stderr).toContain('model is not supported');
});
test('a broken CLI found by the role probe exits 2 and never dispatches', () => {
const r = run('claude', home(), { FAKE_PROBE: 'broken' });
expect(r.status).toBe(2);
+1 -1
View File
@@ -99,7 +99,7 @@ exec ${quote(Bun.which('cat')!)} "$@"
}));
};
// #2914: the selected model and its source are printed before the paid call.
const selected = (cached = false) => `CODEX_MODEL: gpt-6-astra (exec; role: plan-review, tier: frontier; source: gstack catalog frontier/openai (verified 2026-10-07))\nAUTH_OK\nMODEL_OK${cached ? ' (cached)' : ''}\n`;
const selected = (cached = false) => `CODEX_MODEL: gpt-6-astra (exec; role: plan-review, tier: frontier; source: gstack catalog frontier/openai (verified 2026-10-07))\nAUTH_OK\nMODEL_OK${cached ? ' (cached)' : ''}\nHINT: If the review rejects the selected model, repair its winning source: gstack-config set model_frontier_openai <model-id>; gstack-config set plan_review_tier smart; gstack-config set plan_review_tier host.\n`;
return { dir, run, stale, staleError, calls, selected,
created: () => fs.existsSync(created) ? fs.readFileSync(created, 'utf8').trim().split('\n') : [],
cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };