@event4u/agent-config 12.0.0 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +44 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/MIGRATION.md +20 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
|
@@ -1,351 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Tier vocabulary + cool-down state.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* ARCHIVED 2026-08-16 — the budget-routing DECISION LAYER and PERMIT LIFECYCLE
|
|
5
|
+
* that used to live here (`pickTier`, `acquireBudgetPermit`, `settlePermit`,
|
|
6
|
+
* `tripCooldown`, and the reserve/lock machinery behind them) were removed by a
|
|
7
|
+
* converged AI-council verdict (anthropic + openai, 2 of 2, 2026-08-16), which
|
|
8
|
+
* reversed the v1 contract locked on 2026-08-03 and retired its pre-registered
|
|
9
|
+
* AC1–AC5. `docs/contracts/budget-routing.md` is the migration record.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* tiers via the same relation.
|
|
11
|
+
* The short reason: `pickTier` required a `routing_switch` input whose sole
|
|
12
|
+
* source — the `subagents.budget_routing` settings key — was DELETED on purpose
|
|
13
|
+
* by always-on orchestration. Wiring it therefore meant inventing a replacement
|
|
14
|
+
* for a category that had been removed deliberately, not finishing an
|
|
15
|
+
* integration. It had zero production callers, and with `session_tier` non-null
|
|
16
|
+
* in 0 of 327 orchestration records there was no basis on which its saving could
|
|
17
|
+
* ever have been measured.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* What stayed, and why: `TIER_ORDER` and `readCooldowns` have a live consumer in
|
|
20
|
+
* `routing_doctor.ts`, which reports cool-down state as a diagnostic. That is
|
|
21
|
+
* monitoring, not routing, and it never depended on the decision layer.
|
|
22
|
+
*
|
|
23
|
+
* Reversing this needs a real change of conditions, not a preference. Both
|
|
24
|
+
* council members attached one: an authoritative per-request tier-selection
|
|
25
|
+
* signal appearing with a named production dispatch point, plus telemetry
|
|
26
|
+
* carrying BOTH the chosen and the realized tier so a saving can be computed at
|
|
27
|
+
* all. Until then the git history holds the implementation.
|
|
20
28
|
*/
|
|
21
29
|
import * as fs from 'node:fs';
|
|
22
30
|
import * as path from 'node:path';
|
|
23
|
-
import { fileURLToPath } from 'node:url';
|
|
24
31
|
|
|
25
32
|
export type BudgetTier = 'cheap' | 'medium' | 'strong';
|
|
26
33
|
|
|
27
|
-
/** Ordered cheapest-first
|
|
34
|
+
/** Ordered cheapest-first. */
|
|
28
35
|
export const TIER_ORDER: readonly BudgetTier[] = ['cheap', 'medium', 'strong'];
|
|
29
36
|
|
|
30
|
-
export interface TierBudgetState {
|
|
31
|
-
/** USD ceiling from cost.budgets.per_tier.<tier>; null = no tier cap. */
|
|
32
|
-
ceiling_usd: number | null;
|
|
33
|
-
/** Ledger-summed spend for this tier in the ceiling's window (USD). */
|
|
34
|
-
spent_usd: number;
|
|
35
|
-
/** Epoch ms until which this tier is cooling after a quota error; 0 = live. */
|
|
36
|
-
cooldown_until_ms: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type BudgetRoutingSwitch = 'ask' | 'auto' | 'off';
|
|
40
|
-
|
|
41
|
-
export interface PickTierInputs {
|
|
42
|
-
/** Cheapest tier the classifier marked adequate (quality floor). */
|
|
43
|
-
adequate_tier: BudgetTier;
|
|
44
|
-
/** Per-tier budget state, complete over TIER_ORDER. */
|
|
45
|
-
budgets: Record<BudgetTier, TierBudgetState>;
|
|
46
|
-
/**
|
|
47
|
-
* Caller-supplied switch. Formerly sourced from the `subagents.budget_routing`
|
|
48
|
-
* settings key, deleted by always-on orchestration (Phase 1) — this
|
|
49
|
-
* function has no production caller today, so the field stays as the
|
|
50
|
-
* DESIGNED input shape; a future wiring supplies its own source.
|
|
51
|
-
*/
|
|
52
|
-
routing_switch: BudgetRoutingSwitch;
|
|
53
|
-
/** Estimated cost of the dispatch (USD) for the availability check. */
|
|
54
|
-
estimated_cost_usd: number;
|
|
55
|
-
/** Clock injection — never Date.now() in the decision path. */
|
|
56
|
-
now_ms: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface PickTierDecision {
|
|
60
|
-
/** 'tier' → dispatch on `tier`; 'session' → run on the session model. */
|
|
61
|
-
route: 'tier' | 'session';
|
|
62
|
-
tier: BudgetTier | null;
|
|
63
|
-
/** 'proceed' | 'ask' (switch=ask and a budget-motivated downshift) */
|
|
64
|
-
action: 'proceed' | 'ask';
|
|
65
|
-
/** Non-empty when the relation degraded (exhausted / cooling / off). */
|
|
66
|
-
notice: string;
|
|
67
|
-
reason: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function tierAvailable(
|
|
71
|
-
s: TierBudgetState,
|
|
72
|
-
est: number,
|
|
73
|
-
now: number,
|
|
74
|
-
): { ok: boolean; why: string } {
|
|
75
|
-
if (s.cooldown_until_ms > now) {
|
|
76
|
-
return { ok: false, why: 'cooling after quota error' };
|
|
77
|
-
}
|
|
78
|
-
if (s.ceiling_usd === null) {
|
|
79
|
-
return { ok: true, why: 'no tier cap' };
|
|
80
|
-
}
|
|
81
|
-
if (s.spent_usd + est > s.ceiling_usd) {
|
|
82
|
-
return { ok: false, why: `ceiling ${s.ceiling_usd} reached (spent ${s.spent_usd})` };
|
|
83
|
-
}
|
|
84
|
-
return { ok: true, why: 'within ceiling' };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** The binding relation from docs/contracts/budget-routing.md. */
|
|
88
|
-
export function pickTier(inp: PickTierInputs): PickTierDecision {
|
|
89
|
-
if (inp.routing_switch === 'off') {
|
|
90
|
-
return {
|
|
91
|
-
route: 'session',
|
|
92
|
-
tier: null,
|
|
93
|
-
action: 'proceed',
|
|
94
|
-
notice: '',
|
|
95
|
-
reason: 'budget_routing off — session model, today\'s behavior',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
const startIdx = TIER_ORDER.indexOf(inp.adequate_tier);
|
|
99
|
-
const skipped: string[] = [];
|
|
100
|
-
for (let i = startIdx; i < TIER_ORDER.length; i += 1) {
|
|
101
|
-
const tier = TIER_ORDER[i] as BudgetTier;
|
|
102
|
-
const avail = tierAvailable(inp.budgets[tier], inp.estimated_cost_usd, inp.now_ms);
|
|
103
|
-
if (avail.ok) {
|
|
104
|
-
const degraded = i > startIdx;
|
|
105
|
-
return {
|
|
106
|
-
route: 'tier',
|
|
107
|
-
tier,
|
|
108
|
-
action: inp.routing_switch === 'ask' ? 'ask' : 'proceed',
|
|
109
|
-
notice: degraded
|
|
110
|
-
? `cheapest adequate tier unavailable (${skipped.join('; ')}) — routed up to ${tier}`
|
|
111
|
-
: '',
|
|
112
|
-
reason: `${tier}: ${avail.why}${degraded ? '' : ' — cheapest adequate tier'}`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
skipped.push(`${tier}: ${avail.why}`);
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
route: 'session',
|
|
119
|
-
tier: null,
|
|
120
|
-
action: 'proceed',
|
|
121
|
-
notice: `all tier budgets unavailable (${skipped.join('; ')}) — session model, work not blocked`,
|
|
122
|
-
reason: 'fail-open: budget machinery degrades savings, never work',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Atomic permit + cool-down (filesystem side) ────────────────────────────
|
|
127
|
-
|
|
128
|
-
export const RESERVE_FILE = 'tier-reserves.jsonl';
|
|
129
37
|
export const COOLDOWN_FILE = 'tier-cooldowns.json';
|
|
130
|
-
export const DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
|
|
131
|
-
|
|
132
|
-
// Reserve lifecycle constants — single source of truth shared with
|
|
133
|
-
// budget.mjs (the second reserve reader) via src/config/budget-routing.json.
|
|
134
|
-
// Duplicating the literal here was the exact two-truths defect the external
|
|
135
|
-
// review flagged; both readers now load the same file.
|
|
136
|
-
const _LIFECYCLE_CONFIG = path.join(
|
|
137
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
138
|
-
'..',
|
|
139
|
-
'..',
|
|
140
|
-
'config',
|
|
141
|
-
'budget-routing.json',
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
function _lifecycle(): { reserve_ttl_ms: number; lock_break_ms: number } {
|
|
145
|
-
try {
|
|
146
|
-
const raw = JSON.parse(fs.readFileSync(_LIFECYCLE_CONFIG, 'utf-8')) as Record<string, unknown>;
|
|
147
|
-
const ttl = typeof raw['reserve_ttl_ms'] === 'number' ? raw['reserve_ttl_ms'] : 600_000;
|
|
148
|
-
const brk = typeof raw['lock_break_ms'] === 'number' ? raw['lock_break_ms'] : 30_000;
|
|
149
|
-
return { reserve_ttl_ms: ttl, lock_break_ms: brk };
|
|
150
|
-
} catch {
|
|
151
|
-
return { reserve_ttl_ms: 600_000, lock_break_ms: 30_000 };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function reserveTtlMs(): number {
|
|
156
|
-
return _lifecycle().reserve_ttl_ms;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface ReserveEntry {
|
|
160
|
-
ts_ms: number;
|
|
161
|
-
tier: BudgetTier;
|
|
162
|
-
est_usd: number;
|
|
163
|
-
status: 'pending' | 'settled';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function _readReserves(reservePath: string): ReserveEntry[] {
|
|
167
|
-
if (!fs.existsSync(reservePath)) {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
const out: ReserveEntry[] = [];
|
|
171
|
-
for (const line of fs.readFileSync(reservePath, 'utf-8').split('\n')) {
|
|
172
|
-
if (!line.trim()) continue;
|
|
173
|
-
try {
|
|
174
|
-
out.push(JSON.parse(line) as ReserveEntry);
|
|
175
|
-
} catch {
|
|
176
|
-
// tolerate a torn line — reserve file is advisory spend state
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Take the reserve lock; break a stale one (crash leftover) once. */
|
|
183
|
-
function _takeLock(lock: string, now_ms: number, lock_break_ms: number): number | null {
|
|
184
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
185
|
-
try {
|
|
186
|
-
return fs.openSync(lock, 'wx');
|
|
187
|
-
} catch {
|
|
188
|
-
try {
|
|
189
|
-
const age = now_ms - fs.statSync(lock).mtimeMs;
|
|
190
|
-
if (age > lock_break_ms) {
|
|
191
|
-
fs.rmSync(lock, { force: true });
|
|
192
|
-
continue; // one retry after breaking the stale lock
|
|
193
|
-
}
|
|
194
|
-
} catch {
|
|
195
|
-
continue; // lock vanished between open and stat — retry once
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Atomic budget reserve — full lifecycle (external review 2026-08-03):
|
|
205
|
-
*
|
|
206
|
-
* acquire → expire (TTL) / settle → compact-on-write
|
|
207
|
-
*
|
|
208
|
-
* Under the lock: live reserves = pending entries younger than the shared
|
|
209
|
-
* TTL (reserves are RACE PROTECTION, not spend accounting — real spend is
|
|
210
|
-
* the ledger's job); the file is COMPACTED to exactly those live entries
|
|
211
|
-
* on every acquire, so it stays bounded; a lock file older than
|
|
212
|
-
* `lock_break_ms` is a crash leftover and is broken with one retry.
|
|
213
|
-
* Returns granted=false (never throws) when the ceiling would be crossed
|
|
214
|
-
* or a fresh lock is genuinely held — the caller falls back per the
|
|
215
|
-
* relation (fail-open at the DECISION layer, conservative at the SPEND
|
|
216
|
-
* layer).
|
|
217
|
-
*/
|
|
218
|
-
export function acquireBudgetPermit(options: {
|
|
219
|
-
tracking_dir: string;
|
|
220
|
-
tier: BudgetTier;
|
|
221
|
-
estimated_cost_usd: number;
|
|
222
|
-
ceiling_usd: number | null;
|
|
223
|
-
spent_usd: number;
|
|
224
|
-
now_ms: number;
|
|
225
|
-
}): { granted: boolean; reason: string } {
|
|
226
|
-
const dir = options.tracking_dir;
|
|
227
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
228
|
-
const lock = path.join(dir, `${RESERVE_FILE}.lock`);
|
|
229
|
-
const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
|
|
230
|
-
const fd = _takeLock(lock, options.now_ms, lock_break_ms);
|
|
231
|
-
if (fd === null) {
|
|
232
|
-
return { granted: false, reason: 'reserve lock busy — treat tier as unavailable this instant' };
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
const reservePath = path.join(dir, RESERVE_FILE);
|
|
236
|
-
const live = _readReserves(reservePath).filter(
|
|
237
|
-
(e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
|
|
238
|
-
);
|
|
239
|
-
const reserved = live
|
|
240
|
-
.filter((e) => e.tier === options.tier)
|
|
241
|
-
.reduce((acc, e) => acc + e.est_usd, 0);
|
|
242
|
-
if (
|
|
243
|
-
options.ceiling_usd !== null &&
|
|
244
|
-
options.spent_usd + reserved + options.estimated_cost_usd > options.ceiling_usd
|
|
245
|
-
) {
|
|
246
|
-
// Compact even on deny — expiry must not depend on a grant.
|
|
247
|
-
fs.writeFileSync(
|
|
248
|
-
reservePath,
|
|
249
|
-
live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
|
|
250
|
-
'utf-8',
|
|
251
|
-
);
|
|
252
|
-
return {
|
|
253
|
-
granted: false,
|
|
254
|
-
reason: `ceiling ${options.ceiling_usd} would be crossed (spent ${options.spent_usd} + reserved ${reserved})`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
live.push({
|
|
258
|
-
ts_ms: options.now_ms,
|
|
259
|
-
tier: options.tier,
|
|
260
|
-
est_usd: options.estimated_cost_usd,
|
|
261
|
-
status: 'pending',
|
|
262
|
-
});
|
|
263
|
-
fs.writeFileSync(
|
|
264
|
-
reservePath,
|
|
265
|
-
live.map((e) => JSON.stringify(e)).join('\n') + '\n',
|
|
266
|
-
'utf-8',
|
|
267
|
-
);
|
|
268
|
-
return { granted: true, reason: 'reserved' };
|
|
269
|
-
} finally {
|
|
270
|
-
fs.closeSync(fd);
|
|
271
|
-
fs.rmSync(lock, { force: true });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Settle the oldest live pending reserve of a tier (call when the real
|
|
277
|
-
* cost lands in the ledger, so the estimate stops counting immediately
|
|
278
|
-
* instead of waiting out the TTL). Settled entries are dropped by the
|
|
279
|
-
* next compaction. Best-effort: lock busy → no-op (the TTL is the
|
|
280
|
-
* backstop that makes settling optional, never load-bearing).
|
|
281
|
-
*/
|
|
282
|
-
export function settlePermit(options: {
|
|
283
|
-
tracking_dir: string;
|
|
284
|
-
tier: BudgetTier;
|
|
285
|
-
now_ms: number;
|
|
286
|
-
}): boolean {
|
|
287
|
-
const lock = path.join(options.tracking_dir, `${RESERVE_FILE}.lock`);
|
|
288
|
-
const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
|
|
289
|
-
const fd = _takeLock(lock, options.now_ms, lock_break_ms);
|
|
290
|
-
if (fd === null) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
const reservePath = path.join(options.tracking_dir, RESERVE_FILE);
|
|
295
|
-
const live = _readReserves(reservePath).filter(
|
|
296
|
-
(e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
|
|
297
|
-
);
|
|
298
|
-
const idx = live.findIndex((e) => e.tier === options.tier);
|
|
299
|
-
if (idx === -1) {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
live.splice(idx, 1);
|
|
303
|
-
fs.writeFileSync(
|
|
304
|
-
reservePath,
|
|
305
|
-
live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
|
|
306
|
-
'utf-8',
|
|
307
|
-
);
|
|
308
|
-
return true;
|
|
309
|
-
} finally {
|
|
310
|
-
fs.closeSync(fd);
|
|
311
|
-
fs.rmSync(lock, { force: true });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
38
|
|
|
315
39
|
/**
|
|
316
|
-
*
|
|
40
|
+
* Read the cool-down map (missing/torn file → all tiers live).
|
|
317
41
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
42
|
+
* NOTE the asymmetry this now has, because it is load-bearing for the caller:
|
|
43
|
+
* nothing in the tree WRITES this file any more — `tripCooldown` was the only
|
|
44
|
+
* writer and it went with the permit lifecycle. `routing_doctor` already
|
|
45
|
+
* accounts for that and reports cool-down state as *unavailable, no producer*
|
|
46
|
+
* rather than reading an all-zero map as a measured "not cooling". Keep that
|
|
47
|
+
* distinction if you ever add a writer back.
|
|
322
48
|
*/
|
|
323
|
-
export function tripCooldown(options: {
|
|
324
|
-
tracking_dir: string;
|
|
325
|
-
tier: BudgetTier;
|
|
326
|
-
now_ms: number;
|
|
327
|
-
cooldown_ms?: number;
|
|
328
|
-
}): number {
|
|
329
|
-
const until = options.now_ms + (options.cooldown_ms ?? DEFAULT_COOLDOWN_MS);
|
|
330
|
-
const p = path.join(options.tracking_dir, COOLDOWN_FILE);
|
|
331
|
-
let state: Record<string, number> = {};
|
|
332
|
-
try {
|
|
333
|
-
state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, number>;
|
|
334
|
-
} catch {
|
|
335
|
-
state = {};
|
|
336
|
-
}
|
|
337
|
-
for (const [k, v] of Object.entries(state)) {
|
|
338
|
-
if (typeof v !== 'number' || v <= options.now_ms) {
|
|
339
|
-
delete state[k];
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
state[options.tier] = until;
|
|
343
|
-
fs.mkdirSync(options.tracking_dir, { recursive: true });
|
|
344
|
-
fs.writeFileSync(p, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
345
|
-
return until;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** Read the cool-down map (missing/torn file → all tiers live). */
|
|
349
49
|
export function readCooldowns(tracking_dir: string): Record<BudgetTier, number> {
|
|
350
50
|
const empty: Record<BudgetTier, number> = { cheap: 0, medium: 0, strong: 0 };
|
|
351
51
|
try {
|
|
@@ -115,7 +115,37 @@ export const DEFAULT_PERPLEXITY_MODEL = 'sonar-pro';
|
|
|
115
115
|
// divergence is a one-line change, not a hunt through inline literals.
|
|
116
116
|
// (`xai` / `perplexity` CLI reuse their API constants: their CLIs are
|
|
117
117
|
// community wrappers around the same paid API, so the values do not diverge.)
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Sentinel: let the codex CLI pick its own model, by omitting `--model`.
|
|
120
|
+
*
|
|
121
|
+
* This is not a placeholder for a value nobody looked up. Measured
|
|
122
|
+
* 2026-08-15 against `codex exec --json` on a ChatGPT-account (subscription)
|
|
123
|
+
* transport, every explicitly named candidate was refused with
|
|
124
|
+
* `400 invalid_request_error: The '<model>' model is not supported when using
|
|
125
|
+
* Codex with a ChatGPT account.` — `gpt-4o`, `gpt-5` (this constant's previous
|
|
126
|
+
* value) and `gpt-5.1-codex` alike. Omitting the flag answered normally.
|
|
127
|
+
*
|
|
128
|
+
* So the only value known to work on this transport is "whatever the CLI
|
|
129
|
+
* chooses", and pinning ANY name here would re-break the seat the next time
|
|
130
|
+
* the vendor rotates its lineup. The label is carried into records as-is so a
|
|
131
|
+
* reader can see that no model was pinned, rather than a name being implied
|
|
132
|
+
* that nobody verified.
|
|
133
|
+
*/
|
|
134
|
+
export const OPENAI_CLI_VENDOR_DEFAULT = 'codex-default';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Models MEASURED to be refused by the codex subscription transport, with the
|
|
138
|
+
* date they were measured. This is a deny-list on purpose: the CLI publishes
|
|
139
|
+
* no allow-list, so the honest claim is "these were rejected when we tried",
|
|
140
|
+
* never "only these are allowed".
|
|
141
|
+
*/
|
|
142
|
+
export const CODEX_MEASURED_UNSERVABLE: ReadonlyMap<string, string> = new Map([
|
|
143
|
+
['gpt-4o', '2026-08-15'],
|
|
144
|
+
['gpt-5', '2026-08-15'],
|
|
145
|
+
['gpt-5.1-codex', '2026-08-15'],
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
|
|
119
149
|
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'claude-sonnet-4-5';
|
|
120
150
|
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
121
151
|
|
|
@@ -665,7 +695,18 @@ export class OpenAIClient extends ExternalAIClient {
|
|
|
665
695
|
|
|
666
696
|
constructor(opts: ApiClientOptions = {}) {
|
|
667
697
|
super();
|
|
668
|
-
|
|
698
|
+
// `codex-default` is the CLI transport's "let the host choose" sentinel,
|
|
699
|
+
// and ONE `model:` field in `.ai-council.yml` feeds both transports —
|
|
700
|
+
// which one a member resolves to is decided at run time, not in the
|
|
701
|
+
// config. The API has no such sentinel (there is no endpoint named
|
|
702
|
+
// `codex-default`), so it resolves to this client's own default instead
|
|
703
|
+
// of being sent verbatim and rejected. Without this, making the shipped
|
|
704
|
+
// template CLI-safe would break every api-transport user in the same
|
|
705
|
+
// commit.
|
|
706
|
+
this.model =
|
|
707
|
+
opts.model === undefined || opts.model === OPENAI_CLI_VENDOR_DEFAULT
|
|
708
|
+
? DEFAULT_OPENAI_MODEL
|
|
709
|
+
: opts.model;
|
|
669
710
|
if (opts.client !== undefined && opts.client !== null) {
|
|
670
711
|
this._client = opts.client;
|
|
671
712
|
return;
|
|
@@ -1252,6 +1293,20 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1252
1293
|
}
|
|
1253
1294
|
}
|
|
1254
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* Refuse a call this transport cannot serve, BEFORE spawning.
|
|
1298
|
+
*
|
|
1299
|
+
* `null` means "nothing known to be wrong" — deliberately not "verified
|
|
1300
|
+
* servable". No vendor CLI here publishes the set of models its
|
|
1301
|
+
* subscription tier accepts, so an allow-list would be invented and would
|
|
1302
|
+
* reject valid pins the day the vendor adds one. What CAN be stated is the
|
|
1303
|
+
* complement: models this estate has MEASURED being rejected. Subclasses
|
|
1304
|
+
* override; the base transport knows of none.
|
|
1305
|
+
*/
|
|
1306
|
+
protected _preflight_transport(): { code: string; detail: string } | null {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1255
1310
|
/** Return text to send on stdin, or `null` to inherit caller's stdin. */
|
|
1256
1311
|
protected _stdin_payload(system_prompt: string, user_prompt: string): string | null {
|
|
1257
1312
|
void system_prompt;
|
|
@@ -1391,6 +1446,26 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1391
1446
|
}
|
|
1392
1447
|
}
|
|
1393
1448
|
|
|
1449
|
+
// 1b. transport pre-flight — refuse BEFORE spending.
|
|
1450
|
+
//
|
|
1451
|
+
// A subscription transport can be structurally unable to serve the
|
|
1452
|
+
// configured model. Discovering that by spawning costs a call against
|
|
1453
|
+
// the daily cap and returns an opaque `exit_1`, which reads as "the CLI
|
|
1454
|
+
// is flaky" rather than "this pin can never work here". A refusal that
|
|
1455
|
+
// names the model and the transport is the difference between a config
|
|
1456
|
+
// bug someone fixes in a minute and a seat that quietly stays dead.
|
|
1457
|
+
const preflight = this._preflight_transport();
|
|
1458
|
+
if (preflight !== null) {
|
|
1459
|
+
return new CouncilResponse({
|
|
1460
|
+
provider: this.name,
|
|
1461
|
+
model: this.model,
|
|
1462
|
+
text: '',
|
|
1463
|
+
latency_ms: _elapsedMs(t0),
|
|
1464
|
+
error: preflight.code,
|
|
1465
|
+
metadata: { cli: true, transport: this.transport, detail: preflight.detail },
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1394
1469
|
// 2. build command + spawn.
|
|
1395
1470
|
const cmd = this._build_command(system_prompt, user_prompt, max_tokens);
|
|
1396
1471
|
const stdinPayload = this._stdin_payload(system_prompt, user_prompt);
|
|
@@ -1604,23 +1679,23 @@ export class AnthropicCliClient extends CliClient {
|
|
|
1604
1679
|
// documented "disable all tools" value.
|
|
1605
1680
|
//
|
|
1606
1681
|
// It also repairs a live failure, which is how the grant was found
|
|
1607
|
-
// rather than reasoned about.
|
|
1608
|
-
//
|
|
1609
|
-
//
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
// In this package that overflowed the model's context window
|
|
1613
|
-
// outright: `is_error: true`, `"the request is ~214331 tokens
|
|
1682
|
+
// rather than reasoned about. AT THE TIME (2026-08-12) the spawn
|
|
1683
|
+
// inherited the caller's cwd, so invoking the council from inside a
|
|
1684
|
+
// repository made the child load that project's own instructions AND
|
|
1685
|
+
// every tool definition. In this package that overflowed the context
|
|
1686
|
+
// window outright: `is_error: true`, `"the request is ~214331 tokens
|
|
1614
1687
|
// (limit 200000) … the rest is system prompt, tool definitions"`,
|
|
1615
|
-
// exit 1, empty stderr —
|
|
1616
|
-
//
|
|
1617
|
-
//
|
|
1618
|
-
// returns the worktree call to exit 0.
|
|
1688
|
+
// exit 1, empty stderr — the member reported unavailable with no
|
|
1689
|
+
// diagnosable cause. Identical call from `/tmp` exit 0, from the
|
|
1690
|
+
// worktree exit 1, and this one flag returns the worktree to exit 0.
|
|
1619
1691
|
//
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1692
|
+
// TENSE CORRECTED 2026-08-16. This block read "`_runSubprocess` sets
|
|
1693
|
+
// none, deliberately" and "dropping the tools RATHER THAN
|
|
1694
|
+
// neutralising the cwd" — both false since `_runSubprocess` set
|
|
1695
|
+
// `cwd: os.tmpdir()` for a DIFFERENT contamination (a member
|
|
1696
|
+
// answering this repo's `end-review-nudge`). Two repairs, one
|
|
1697
|
+
// cause, neither aware of the other, so a reader here concluded
|
|
1698
|
+
// the cwd is inherited. Both are live; this flag bounds what a member may DO.
|
|
1624
1699
|
'--tools',
|
|
1625
1700
|
'',
|
|
1626
1701
|
'--append-system-prompt',
|
|
@@ -1726,9 +1801,46 @@ export class OpenAICliClient extends CliClient {
|
|
|
1726
1801
|
void system_prompt;
|
|
1727
1802
|
void user_prompt;
|
|
1728
1803
|
void max_tokens;
|
|
1804
|
+
// `--skip-git-repo-check` is not a convenience flag. Without it codex
|
|
1805
|
+
// refuses any directory it does not consider trusted — captured
|
|
1806
|
+
// `stderr_tail`: `Not inside a trusted directory and
|
|
1807
|
+
// --skip-git-repo-check was not specified.` A worktree path is never
|
|
1808
|
+
// trusted, which is the whole explanation for the standing observation
|
|
1809
|
+
// that the openai seat dies in a worktree while working from the main
|
|
1810
|
+
// checkout. The council reads a prompt and writes nothing through this
|
|
1811
|
+
// process, so the trust gate protects nothing here that the council's
|
|
1812
|
+
// own gates do not already cover.
|
|
1813
|
+
//
|
|
1729
1814
|
// `-` is codex's documented "read the prompt from stdin" positional;
|
|
1730
1815
|
// the payload itself is assembled in `_stdin_payload`.
|
|
1731
|
-
|
|
1816
|
+
const modelArgs =
|
|
1817
|
+
this.model === OPENAI_CLI_VENDOR_DEFAULT ? [] : ['--model', this.model];
|
|
1818
|
+
return [this.binary, 'exec', '--json', '--skip-git-repo-check', ...modelArgs, '-'];
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Refuse a pin this estate has measured the transport rejecting.
|
|
1823
|
+
*
|
|
1824
|
+
* The remedy is named because it is not guessable: removing the `model:`
|
|
1825
|
+
* line is the fix, and "remove a setting" is the last thing an operator
|
|
1826
|
+
* tries when a member is silent.
|
|
1827
|
+
*/
|
|
1828
|
+
protected override _preflight_transport(): { code: string; detail: string } | null {
|
|
1829
|
+
const measuredOn = CODEX_MEASURED_UNSERVABLE.get(this.model);
|
|
1830
|
+
if (measuredOn === undefined) return null;
|
|
1831
|
+
return {
|
|
1832
|
+
code: 'model_unsupported_on_transport',
|
|
1833
|
+
detail:
|
|
1834
|
+
`openai member pins model \`${this.model}\`, which the codex CLI ` +
|
|
1835
|
+
`(subscription transport) refused when measured on ${measuredOn}: ` +
|
|
1836
|
+
`"The '${this.model}' model is not supported when using Codex with a ` +
|
|
1837
|
+
'ChatGPT account." The CLI publishes no list of models it DOES accept, ' +
|
|
1838
|
+
'so there is no supported set to offer here — the one configuration ' +
|
|
1839
|
+
'measured to work is no pin at all. Fix: set `model: codex-default` on ' +
|
|
1840
|
+
'the openai member. Do NOT delete the `model:` line — an enabled member ' +
|
|
1841
|
+
'with no model fails config load, so that takes EVERY provider to NOT ' +
|
|
1842
|
+
'CONFIGURED. A pin is reachable only on the API transport.',
|
|
1843
|
+
};
|
|
1732
1844
|
}
|
|
1733
1845
|
|
|
1734
1846
|
/**
|
|
@@ -1752,6 +1864,7 @@ export class OpenAICliClient extends CliClient {
|
|
|
1752
1864
|
let text = '';
|
|
1753
1865
|
let input_tokens = 0;
|
|
1754
1866
|
let output_tokens = 0;
|
|
1867
|
+
let failure: string | null = null;
|
|
1755
1868
|
const meta: Record<string, unknown> = {};
|
|
1756
1869
|
for (let line of _splitlines(stdout)) {
|
|
1757
1870
|
line = line.trim();
|
|
@@ -1794,6 +1907,31 @@ export class OpenAICliClient extends CliClient {
|
|
|
1794
1907
|
text = chunks.join('\n').trim();
|
|
1795
1908
|
}
|
|
1796
1909
|
}
|
|
1910
|
+
// The FLAT shape, and it is the one the CLI emits today:
|
|
1911
|
+
// {"item":{"type":"agent_message","text":"OK"}}
|
|
1912
|
+
// The `content[]` branch above is the older nested form. Both
|
|
1913
|
+
// are read because this adapter has no way to pin a CLI
|
|
1914
|
+
// version, and reading only the nested one is why the seat
|
|
1915
|
+
// returned an empty string with NO error for every call —
|
|
1916
|
+
// measured 2026-08-15, and indistinguishable downstream from
|
|
1917
|
+
// a model that answered with nothing. Only an `agent_message`
|
|
1918
|
+
// is taken: an `error` item also carries `text`-adjacent
|
|
1919
|
+
// fields, and treating one as an answer would turn the
|
|
1920
|
+
// skills-budget warning into the member's response.
|
|
1921
|
+
//
|
|
1922
|
+
// LAST message wins, never the first. A codex turn emits
|
|
1923
|
+
// several `agent_message` items — a preamble ("I'll check X
|
|
1924
|
+
// first…") and then the answer — so keeping the first one
|
|
1925
|
+
// truncates the response to its throat-clearing. Measured
|
|
1926
|
+
// 2026-08-15 on a real council run: 1,479 output tokens
|
|
1927
|
+
// billed, 134 characters captured. A one-word probe cannot
|
|
1928
|
+
// catch this, which is why it survived the first fix.
|
|
1929
|
+
if (_dictGet(itemObj, 'type', undefined) === 'agent_message') {
|
|
1930
|
+
const flat = _pyStr(_dictGet(itemObj, 'text', '')).trim();
|
|
1931
|
+
if (flat) {
|
|
1932
|
+
text = flat;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1797
1935
|
if (_pyTruthy(_dictGet(itemObj, 'id', undefined))) {
|
|
1798
1936
|
meta['item_id'] = _pyStr(itemObj['id']);
|
|
1799
1937
|
}
|
|
@@ -1812,8 +1950,49 @@ export class OpenAICliClient extends CliClient {
|
|
|
1812
1950
|
if (_pyTruthy(_dictGet(ev, 'session_id', undefined))) {
|
|
1813
1951
|
meta['session_id'] = _pyStr(ev['session_id']);
|
|
1814
1952
|
}
|
|
1953
|
+
} else if (event_type === 'turn.failed') {
|
|
1954
|
+
// The turn's real cause lives ONLY here, and codex still exits
|
|
1955
|
+
// 0. Without this branch the response is an empty text, which
|
|
1956
|
+
// the caller reports as `no response` — indistinguishable from
|
|
1957
|
+
// a model that answered with nothing. Measured 2026-08-15: a
|
|
1958
|
+
// pinned `gpt-4o` produced exactly this shape, and the seat
|
|
1959
|
+
// read as flaky for weeks instead of as misconfigured.
|
|
1960
|
+
let err = _dictGet(ev, 'error', null);
|
|
1961
|
+
if (!_pyTruthy(err)) err = {};
|
|
1962
|
+
if (_isPlainObject(err)) {
|
|
1963
|
+
const raw = _pyStr(_dictGet(err as Record<string, unknown>, 'message', ''));
|
|
1964
|
+
if (raw) failure = raw;
|
|
1965
|
+
}
|
|
1815
1966
|
}
|
|
1816
1967
|
}
|
|
1968
|
+
|
|
1969
|
+
if (failure !== null && !text) {
|
|
1970
|
+
const unsupported = /not supported when using Codex/i.test(failure);
|
|
1971
|
+
return new CouncilResponse({
|
|
1972
|
+
provider: this.name,
|
|
1973
|
+
model: this.model,
|
|
1974
|
+
text: '',
|
|
1975
|
+
input_tokens,
|
|
1976
|
+
output_tokens,
|
|
1977
|
+
error: unsupported ? 'model_unsupported_on_transport' : 'turn_failed',
|
|
1978
|
+
metadata: {
|
|
1979
|
+
...meta,
|
|
1980
|
+
// The vendor's own sentence, kept because it names the
|
|
1981
|
+
// model and the account type — the two facts an operator
|
|
1982
|
+
// needs and neither of which the error code carries.
|
|
1983
|
+
detail: failure.slice(0, 400),
|
|
1984
|
+
...(unsupported
|
|
1985
|
+
? {
|
|
1986
|
+
hint:
|
|
1987
|
+
'the codex subscription transport serves no explicitly pinned ' +
|
|
1988
|
+
'model measured so far — set `model: codex-default` on the ' +
|
|
1989
|
+
'openai member; deleting the line fails config load',
|
|
1990
|
+
}
|
|
1991
|
+
: {}),
|
|
1992
|
+
},
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1817
1996
|
return new CouncilResponse({
|
|
1818
1997
|
provider: this.name,
|
|
1819
1998
|
model: this.model,
|