fix(cost,app-server): footer cost chip shows confirmed dollars only; file revert follows the user's posture again

Cost chip (founder 2026-11-08, CURRENT_DECISIONS 86): the metrics-line chip prints
the confirmed amount and nothing else. A partly priced session shows exactly its
confirmed amount; a session with nothing priced shows no cost segment. The
"subtotal {amount} + unknown ({reasons})", "saved subtotal" and "cost: unknown
({reasons})" templates and format_unpriced_reasons are removed from the engine
and every locale. The price view still says "unknown" for a session with no
confirmed price, and /cost remains the one place that lists each unpriced turn;
its reasons are reworded in en and zh-Hans ("the provider never confirmed the
request finished", "this session predates cost tracking", "still running").
Accounting is unchanged: unpriced turns stay excluded, never guessed.

File revert: 74713ec7cd added a trust gate to POST /v1/threads/{id}/file-revert,
undoing 6a6d9bf298 ("revert follows the user's own choices"). The route's
checks are the restore point the thread owns and the reviewed hash, as before.
The gate, its CHANGELOG line and the docs/RUNTIME_API.md "trust always" row are
removed, and the route test is renamed to validate body then ownership.

Verification (focused, macOS, existing tests only): codewhale-localization lib
55 passed; codewhale-engine route_billing 52 passed; codewhale-app-server
file_revert 2 passed; codewhale-tui cost 36 passed, statusline 4 passed,
provider_switch_keeps 1 passed; cargo fmt --check clean; sync-changelog --check
clean. Not run: Windows, hosted CI, a real TUI or desktop journey.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-11-08 13:36:17 -08:00
1 parent 74713ec7cd
commit 0a626fdab8
26 files changed
+85 -258

No files matched your search

+1 -1
View File
@@ -245,6 +245,7 @@ until the matching tag, packages, checksums, and release assets exist.
### Changed
- **The footer cost chip shows confirmed dollars only.** A session with some unpriced turns shows exactly the confirmed amount, such as `$0.34`, instead of `subtotal 0.34 + unknown (request outcome unknown)`, and a session with nothing priced shows no cost at all instead of `cost: unknown (…)`. The accounting is unchanged: unpriced turns stay excluded rather than guessed, and `/cost` still lists each one with its reason, now in plain words ("the provider never confirmed the request finished", "this session predates cost tracking"). The price view says `unknown` for a session with no confirmed price.
- **Sub-agents and goals by default.** The constitution now tells the agent to start independent parts of a request as parallel sub-agents in one message, keep working while their results arrive on their own, and end the turn when only waiting remains; to give each child a complete prompt and its own files; to inspect a write-capable child's evidence, review the diff and run the checks itself; to have an independent reviewer try to refute a non-trivial change before calling it done; and to start a goal for any request, in any language, whose verifiable end state needs more than one turn. Sub-agents, workflows and goals within the current request are in scope; spending real money (purchases, paid providers, billing) still needs the user's express authorization. Cost alone is not a reason to stop; only limits the user or configuration sets apply. **This changes the session-pinned prompt and tool descriptions, so each session re-pins its prompt cache once after upgrade.**
- **Operate's contract is retired.** The Engine no longer appends the Operate contract to a session; its review, cost and evidence rules now live in the constitution for every session. Saved sessions that contain it still replay.
- **The Operate mode is gone; Work and Plan are the two modes.** Tab cycles between them, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
@@ -375,7 +376,6 @@ until the matching tag, packages, checksums, and release assets exist.
### Fixed
- **Reverting a file requires a trusted thread.** The Runtime API's file-revert route refuses an untrusted thread with a `409` that names `/trust` or Full Access, matching the TUI's undo gate, and answers that refusal before it looks at snapshot ownership. Before, an untrusted thread that named a restore point it owned could rewrite workspace files.
- **Anthropic and Messages-compatible routes accept any tool name.** Tool names that Anthropic's pattern rejects, such as `web.run` from a loaded tool group, are encoded on the wire and decoded in replies, as the chat and responses routes already did. Before, one such name made every request fail with "tools.N.custom.name: String should match pattern", which stopped sub-agents that had loaded it.
- **A large sub-agent fan-out no longer exhausts the usage journal.** The per-session limit of live usage receipts rises from 64 to 256, above the workflow ceiling of 128, so a full fan-out keeps its cost receipts instead of running with cost unknown.
- **A turn that stalls before it starts is now bounded.** The stall watchdog covers the setup before the first model request, so a wedged start is logged, cancelled after 180 seconds with a stated reason, and leaves a stall record instead of waiting silently. A warning names the case where all four blocking-work slots are held. Refs #6184.
-10
View File
@@ -9894,16 +9894,6 @@ async fn revert_thread_file(
.thread_restore_guard(&id)
.await
.map_err(map_thread_err)?;
// Reverting is a workspace mutation, gated where the TUI gates the same
// operation and before ownership, so an untrusted caller gets the
// actionable refusal instead of a hint about another thread's snapshots.
if !(thread.trust_mode || thread.auto_approve) {
return Err(ApiError::conflict(
"Refusing to revert workspace files outside trusted mode. \
Turn on /trust or switch this thread to Full Access, then revert again.",
)
.with_code(PATCH_UNDO_UNTRUSTED));
}
// The restore points this thread owns: the receipts on its own turns
// (including turns a fork cloned). Read under the restore reservation, so
// no turn is recording one meanwhile.
+8 -20
View File
@@ -9006,13 +9006,13 @@ async fn file_revert_route_probes_as_available_and_404s_unknown_threads() -> Res
Ok(())
}
/// Route-level contract for the new destructive endpoint: malformed bodies
/// are 422/400 before any thread state is read, an untrusted thread is a 409
/// even with a well-formed request, and a trusted thread without a bound
/// restore point it owns is a 409 that names the reason. No file is touched in
/// any case.
/// Route-level contract for the destructive endpoint: malformed bodies are
/// 422/400 before any thread state is read, and a thread that did not record
/// the named restore point is a 409 that names the reason. Reverting follows
/// the user's own choices (`6a6d9bf2980`), so posture is not a gate; the exact
/// snapshot and reviewed-hash checks are. No file is touched in any case.
#[tokio::test]
async fn file_revert_route_validates_body_then_trust_then_ownership() -> Result<()> {
async fn file_revert_route_validates_body_then_ownership() -> Result<()> {
// The route opens the snapshot store under the resolved home. Seal the
// process environment and own that home, so a concurrent test's temporary
// HOME (deleted when it ends) can never host or remove this store mid-request.
@@ -9054,20 +9054,8 @@ async fn file_revert_route_validates_body_then_trust_then_ownership() -> Result<
let resp = client.post(&url).json(&bad).send().await?;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "{bad}");
}
// Untrusted thread: 409 that says how to proceed.
let resp = client.post(&url).json(&well_formed).send().await?;
assert_eq!(resp.status(), StatusCode::CONFLICT);
let text = resp.text().await?;
assert!(text.contains("trusted mode"), "got: {text}");
// Trusted, but the thread recorded no such restore point: still a 409
// with the reason.
client
.patch(format!("http://{addr}/v1/threads/{thread_id}"))
.json(&json!({ "trust_mode": true }))
.send()
.await?
.error_for_status()?;
// A well-formed request for a restore point this thread does not own:
// a 409 that names the reason, whatever the thread's posture.
let resp = client.post(&url).json(&well_formed).send().await?;
assert_eq!(resp.status(), StatusCode::CONFLICT);
let text = resp.text().await?;
+13 -47
View File
@@ -928,22 +928,10 @@ pub fn usage_chip(
pub fn format_usage_chip(chip: &UsageChip, locale: Locale) -> Option<String> {
match chip {
UsageChip::Money(amount) => Some(amount.clone()),
UsageChip::PricedSubtotal {
amount,
legacy,
reasons,
} => Some(
tr(
locale,
if *legacy {
MessageId::CostChipSavedSubtotal
} else {
MessageId::CostChipSubtotal
},
)
.replace("{amount}", amount)
.replace("{reasons}", &format_unpriced_reasons(reasons, locale)),
),
// The glance shows confirmed money only. When part of the session has
// no authoritative price, the confirmed dollars still stand on their
// own; the cost report, not this line, explains what is missing.
UsageChip::PricedSubtotal { amount, .. } => Some(amount.clone()),
UsageChip::Allowance { label, used_pct } => Some(match used_pct {
Some(pct) => tr(locale, MessageId::CostChipAllowancePercent)
.replace("{plan}", label)
@@ -951,30 +939,13 @@ pub fn format_usage_chip(chip: &UsageChip, locale: Locale) -> Option<String> {
None => tr(locale, MessageId::CostChipAllowance).replace("{plan}", label),
}),
UsageChip::Local => Some(tr(locale, MessageId::CostChipLocal).into_owned()),
UsageChip::Unknown(reasons) => Some(
tr(locale, MessageId::CostChipUnknown)
.replace("{reasons}", &format_unpriced_reasons(reasons, locale)),
),
// Nothing confirmed to show: an unpriced session has no dollars, and
// inventing a label for their absence is the noise this replaced.
UsageChip::Unknown(_) => None,
UsageChip::Hidden => None,
}
}
/// The same saved receipt explains missing coverage in every cost surface.
#[must_use]
pub fn format_unpriced_reasons(reasons: &[UnpricedReason], locale: Locale) -> String {
if reasons.is_empty() {
return tr(locale, UnpricedReason::UnrecordedCoverage.message_id()).into_owned();
}
let mut descriptions = Vec::new();
for reason in reasons {
let text = tr(locale, reason.message_id());
if !descriptions.contains(&text) {
descriptions.push(text);
}
}
descriptions.join(", ")
}
fn custom_billing_unknown(config: &ProviderConfig) -> bool {
// A custom OpenAI-compatible endpoint with no explicit pay mode and no
// priced catalog is treated as unknown rather than inventing metered
@@ -2251,13 +2222,9 @@ mod tests {
);
assert_eq!(chip, UsageChip::Unknown(vec![UnpricedReason::NoPricingRow]));
assert_eq!(
format_usage_chip(&chip, codewhale_localization::Locale::En).as_deref(),
Some("cost: unknown (rate unavailable)")
);
assert!(
!format_usage_chip(&chip, codewhale_localization::Locale::En)
.unwrap_or_default()
.contains('$')
format_usage_chip(&chip, codewhale_localization::Locale::En),
None,
"an unpriced turn shows no money"
);
let unknown_billing = usage_chip(
@@ -2272,10 +2239,9 @@ mod tests {
unknown_billing,
UsageChip::Unknown(vec![UnpricedReason::UnknownBillingBasis])
);
assert!(
!format_usage_chip(&unknown_billing, codewhale_localization::Locale::En)
.unwrap_or_default()
.contains('$')
assert_eq!(
format_usage_chip(&unknown_billing, codewhale_localization::Locale::En),
None
);
}
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "el proveïdor ha omès l’ús",
"CostReasonLegacyUsageUnverified": "ús antic no verificat",
"CostReasonUsageCorrectionUnverified": "correcció d’ús no verificada",
"CostChipUnknown": "cost: desconegut ({reasons})",
"CostChipSubtotal": "subtotal {amount} + desconegut ({reasons})",
"CostChipSavedSubtotal": "subtotal desat {amount} + desconegut ({reasons})",
"CostChipLocal": "cost: local",
"CostChipAllowance": "ús: {plan}",
"CostChipAllowancePercent": "ús: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "Anbieter hat keine Nutzungsdaten gemeldet",
"CostReasonLegacyUsageUnverified": "ältere Nutzungsdaten ungeprüft",
"CostReasonUsageCorrectionUnverified": "Nutzungskorrektur ungeprüft",
"CostChipUnknown": "Kosten: unbekannt ({reasons})",
"CostChipSubtotal": "Zwischensumme {amount} + unbekannt ({reasons})",
"CostChipSavedSubtotal": "gespeicherte Zwischensumme {amount} + unbekannt ({reasons})",
"CostChipLocal": "Kosten: lokal",
"CostChipAllowance": "Nutzung: {plan}",
"CostChipAllowancePercent": "Nutzung: {plan} · {percent}%",
+3 -6
View File
@@ -2533,20 +2533,17 @@
"CostReasonRetiredAlias": "retired model alias",
"CostReasonTierMissing": "pricing tier unavailable",
"CostReasonRoutingDependent": "upstream rate not established",
"CostReasonCoverageMissing": "saved coverage unavailable",
"CostReasonCoverageMissing": "this session predates cost tracking",
"CostReasonTokenRateMissing": "token class rate unavailable",
"CostReasonInvalidRate": "invalid rate",
"CostReasonCurrencyMissing": "currency rate unavailable",
"CostReasonUsageConflict": "conflicting token counts",
"CostReasonUsageBreakdown": "token usage breakdown unavailable",
"CostReasonOutcomeUnknown": "request outcome unknown",
"CostReasonInProgress": "request in progress",
"CostReasonOutcomeUnknown": "the provider never confirmed the request finished",
"CostReasonInProgress": "still running",
"CostReasonProviderUsageMissing": "provider omitted usage",
"CostReasonLegacyUsageUnverified": "legacy usage unverified",
"CostReasonUsageCorrectionUnverified": "usage correction unverified",
"CostChipUnknown": "cost: unknown ({reasons})",
"CostChipSubtotal": "subtotal {amount} + unknown ({reasons})",
"CostChipSavedSubtotal": "saved subtotal {amount} + unknown ({reasons})",
"CostChipLocal": "cost: local",
"CostChipAllowance": "usage: {plan}",
"CostChipAllowancePercent": "usage: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "el proveedor omitió el uso",
"CostReasonLegacyUsageUnverified": "uso anterior sin verificar",
"CostReasonUsageCorrectionUnverified": "corrección de uso sin verificar",
"CostChipUnknown": "costo: desconocido ({reasons})",
"CostChipSubtotal": "subtotal {amount} + desconocido ({reasons})",
"CostChipSavedSubtotal": "subtotal guardado {amount} + desconocido ({reasons})",
"CostChipLocal": "costo: local",
"CostChipAllowance": "uso: {plan}",
"CostChipAllowancePercent": "uso: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "utilisation non transmise par le fournisseur",
"CostReasonLegacyUsageUnverified": "ancienne utilisation non vérifiée",
"CostReasonUsageCorrectionUnverified": "correction d’utilisation non vérifiée",
"CostChipUnknown": "coût : inconnu ({reasons})",
"CostChipSubtotal": "sous-total {amount} + inconnu ({reasons})",
"CostChipSavedSubtotal": "sous-total enregistré {amount} + inconnu ({reasons})",
"CostChipLocal": "coût : local",
"CostChipAllowance": "utilisation : {plan}",
"CostChipAllowancePercent": "utilisation : {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "प्रदाता ने उपयोग नहीं बताया",
"CostReasonLegacyUsageUnverified": "पुराने उपयोग की पुष्टि नहीं हुई",
"CostReasonUsageCorrectionUnverified": "उपयोग सुधार की पुष्टि नहीं हुई",
"CostChipUnknown": "लागत: अज्ञात ({reasons})",
"CostChipSubtotal": "उप-योग {amount} + अज्ञात ({reasons})",
"CostChipSavedSubtotal": "सहेजा गया उप-योग {amount} + अज्ञात ({reasons})",
"CostChipLocal": "लागत: स्थानीय",
"CostChipAllowance": "उपयोग: {plan}",
"CostChipAllowancePercent": "उपयोग: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "penyedia tidak melaporkan penggunaan",
"CostReasonLegacyUsageUnverified": "penggunaan lama belum diverifikasi",
"CostReasonUsageCorrectionUnverified": "koreksi penggunaan belum diverifikasi",
"CostChipUnknown": "biaya: belum diketahui ({reasons})",
"CostChipSubtotal": "subtotal {amount} + belum diketahui ({reasons})",
"CostChipSavedSubtotal": "subtotal tersimpan {amount} + belum diketahui ({reasons})",
"CostChipLocal": "biaya: lokal",
"CostChipAllowance": "penggunaan: {plan}",
"CostChipAllowancePercent": "penggunaan: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "プロバイダーが使用量を未報告",
"CostReasonLegacyUsageUnverified": "旧形式の使用量が未検証",
"CostReasonUsageCorrectionUnverified": "使用量の修正が未検証",
"CostChipUnknown": "費用: 不明({reasons})",
"CostChipSubtotal": "小計 {amount} + 不明({reasons})",
"CostChipSavedSubtotal": "保存済み小計 {amount} + 不明({reasons})",
"CostChipLocal": "費用: ローカル",
"CostChipAllowance": "使用量: {plan}",
"CostChipAllowancePercent": "使用量: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "공급자가 사용량을 보고하지 않음",
"CostReasonLegacyUsageUnverified": "이전 사용량이 검증되지 않음",
"CostReasonUsageCorrectionUnverified": "사용량 수정이 검증되지 않음",
"CostChipUnknown": "비용: 알 수 없음 ({reasons})",
"CostChipSubtotal": "소계 {amount} + 알 수 없음 ({reasons})",
"CostChipSavedSubtotal": "저장된 소계 {amount} + 알 수 없음 ({reasons})",
"CostChipLocal": "비용: 로컬",
"CostChipAllowance": "사용량: {plan}",
"CostChipAllowancePercent": "사용량: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "o provedor omitiu o uso",
"CostReasonLegacyUsageUnverified": "uso antigo não verificado",
"CostReasonUsageCorrectionUnverified": "correção de uso não verificada",
"CostChipUnknown": "custo: desconhecido ({reasons})",
"CostChipSubtotal": "subtotal {amount} + desconhecido ({reasons})",
"CostChipSavedSubtotal": "subtotal salvo {amount} + desconhecido ({reasons})",
"CostChipLocal": "custo: local",
"CostChipAllowance": "uso: {plan}",
"CostChipAllowancePercent": "uso: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "провайдер не сообщил расход",
"CostReasonLegacyUsageUnverified": "старые данные расхода не проверены",
"CostReasonUsageCorrectionUnverified": "корректировка расхода не проверена",
"CostChipUnknown": "стоимость: неизвестна ({reasons})",
"CostChipSubtotal": "подытог {amount} + неизвестно ({reasons})",
"CostChipSavedSubtotal": "сохранённый подытог {amount} + неизвестно ({reasons})",
"CostChipLocal": "стоимость: локально",
"CostChipAllowance": "использование: {plan}",
"CostChipAllowancePercent": "использование: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "провайдер не повідомив використання",
"CostReasonLegacyUsageUnverified": "старі дані використання не перевірено",
"CostReasonUsageCorrectionUnverified": "коригування використання не перевірено",
"CostChipUnknown": "вартість: невідома ({reasons})",
"CostChipSubtotal": "проміжний підсумок {amount} + невідомо ({reasons})",
"CostChipSavedSubtotal": "збережений проміжний підсумок {amount} + невідомо ({reasons})",
"CostChipLocal": "вартість: локально",
"CostChipAllowance": "використання: {plan}",
"CostChipAllowancePercent": "використання: {plan} · {percent}%",
-3
View File
@@ -2544,9 +2544,6 @@
"CostReasonProviderUsageMissing": "nhà cung cấp không báo cáo mức sử dụng",
"CostReasonLegacyUsageUnverified": "mức sử dụng cũ chưa được xác minh",
"CostReasonUsageCorrectionUnverified": "điều chỉnh mức sử dụng chưa được xác minh",
"CostChipUnknown": "chi phí: chưa rõ ({reasons})",
"CostChipSubtotal": "tạm tính {amount} + chưa rõ ({reasons})",
"CostChipSavedSubtotal": "tạm tính đã lưu {amount} + chưa rõ ({reasons})",
"CostChipLocal": "chi phí: cục bộ",
"CostChipAllowance": "mức dùng: {plan}",
"CostChipAllowancePercent": "mức dùng: {plan} · {percent}%",
+3 -6
View File
@@ -2533,20 +2533,17 @@
"CostReasonRetiredAlias": "模型别名已停用",
"CostReasonTierMissing": "暂无对应计价档位",
"CostReasonRoutingDependent": "尚未确认上游费率",
"CostReasonCoverageMissing": "缺少已保存的计费覆盖记录",
"CostReasonCoverageMissing": "本次会话早于费用记录功能",
"CostReasonTokenRateMissing": "缺少 token 类别费率",
"CostReasonInvalidRate": "费率无效",
"CostReasonCurrencyMissing": "缺少该币种费率",
"CostReasonUsageConflict": "token 计数冲突",
"CostReasonUsageBreakdown": "token 用量明细不可用",
"CostReasonOutcomeUnknown": "请求结果未知",
"CostReasonInProgress": "请求进行中",
"CostReasonOutcomeUnknown": "提供商未确认该请求是否完成",
"CostReasonInProgress": "仍在运行",
"CostReasonProviderUsageMissing": "提供商未返回用量",
"CostReasonLegacyUsageUnverified": "旧版用量未经核实",
"CostReasonUsageCorrectionUnverified": "用量修正未经核实",
"CostChipUnknown": "费用:未知({reasons})",
"CostChipSubtotal": "小计 {amount} + 未知({reasons})",
"CostChipSavedSubtotal": "已保存小计 {amount} + 未知({reasons})",
"CostChipLocal": "费用:本地",
"CostChipAllowance": "用量:{plan}",
"CostChipAllowancePercent": "用量:{plan} · {percent}%",
-3
View File
@@ -2542,9 +2542,6 @@
"CostReasonProviderUsageMissing": "供應商未回傳用量",
"CostReasonLegacyUsageUnverified": "舊版用量未經核實",
"CostReasonUsageCorrectionUnverified": "用量修正未經核實",
"CostChipUnknown": "費用:未知({reasons})",
"CostChipSubtotal": "小計 {amount} + 未知({reasons})",
"CostChipSavedSubtotal": "已儲存小計 {amount} + 未知({reasons})",
"CostChipLocal": "費用:本機",
"CostChipAllowance": "用量:{plan}",
"CostChipAllowancePercent": "用量:{plan} · {percent}%",
-6
View File
@@ -194,9 +194,6 @@ pub enum MessageId {
CostReasonProviderUsageMissing,
CostReasonLegacyUsageUnverified,
CostReasonUsageCorrectionUnverified,
CostChipUnknown,
CostChipSubtotal,
CostChipSavedSubtotal,
CostChipLocal,
CostChipAllowance,
CostChipAllowancePercent,
@@ -3140,9 +3137,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CostReasonProviderUsageMissing,
MessageId::CostReasonLegacyUsageUnverified,
MessageId::CostReasonUsageCorrectionUnverified,
MessageId::CostChipUnknown,
MessageId::CostChipSubtotal,
MessageId::CostChipSavedSubtotal,
MessageId::CostChipLocal,
MessageId::CostChipAllowance,
MessageId::CostChipAllowancePercent,
+1 -1
View File
@@ -245,6 +245,7 @@ until the matching tag, packages, checksums, and release assets exist.
### Changed
- **The footer cost chip shows confirmed dollars only.** A session with some unpriced turns shows exactly the confirmed amount, such as `$0.34`, instead of `subtotal 0.34 + unknown (request outcome unknown)`, and a session with nothing priced shows no cost at all instead of `cost: unknown (…)`. The accounting is unchanged: unpriced turns stay excluded rather than guessed, and `/cost` still lists each one with its reason, now in plain words ("the provider never confirmed the request finished", "this session predates cost tracking"). The price view says `unknown` for a session with no confirmed price.
- **Sub-agents and goals by default.** The constitution now tells the agent to start independent parts of a request as parallel sub-agents in one message, keep working while their results arrive on their own, and end the turn when only waiting remains; to give each child a complete prompt and its own files; to inspect a write-capable child's evidence, review the diff and run the checks itself; to have an independent reviewer try to refute a non-trivial change before calling it done; and to start a goal for any request, in any language, whose verifiable end state needs more than one turn. Sub-agents, workflows and goals within the current request are in scope; spending real money (purchases, paid providers, billing) still needs the user's express authorization. Cost alone is not a reason to stop; only limits the user or configuration sets apply. **This changes the session-pinned prompt and tool descriptions, so each session re-pins its prompt cache once after upgrade.**
- **Operate's contract is retired.** The Engine no longer appends the Operate contract to a session; its review, cost and evidence rules now live in the constitution for every session. Saved sessions that contain it still replay.
- **The Operate mode is gone; Work and Plan are the two modes.** Tab cycles between them, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
@@ -375,7 +376,6 @@ until the matching tag, packages, checksums, and release assets exist.
### Fixed
- **Reverting a file requires a trusted thread.** The Runtime API's file-revert route refuses an untrusted thread with a `409` that names `/trust` or Full Access, matching the TUI's undo gate, and answers that refusal before it looks at snapshot ownership. Before, an untrusted thread that named a restore point it owned could rewrite workspace files.
- **Anthropic and Messages-compatible routes accept any tool name.** Tool names that Anthropic's pattern rejects, such as `web.run` from a loaded tool group, are encoded on the wire and decoded in replies, as the chat and responses routes already did. Before, one such name made every request fail with "tools.N.custom.name: String should match pattern", which stopped sub-agents that had loaded it.
- **A large sub-agent fan-out no longer exhausts the usage journal.** The per-session limit of live usage receipts rises from 64 to 256, above the workflow ceiling of 128, so a full fan-out keeps its cost receipts instead of running with cost unknown.
- **A turn that stalls before it starts is now bounded.** The stall watchdog covers the setup before the first model request, so a wedged start is logged, cancelled after 180 seconds with a stated reason, and leaves a stall record instead of waiting silently. A warning names the case where all four blocking-work slots are held. Refs #6184.
+12 -5
View File
@@ -5140,17 +5140,24 @@ impl App {
/// Price is one number, everywhere (design §2.11 item 5): the session
/// cost as the footer chip, the price view, and the roster print it.
/// Money routes print the amount; subscription, local, and unknown
/// routes print the same words the chip uses; a metered route that has
/// not spent yet prints `$0.00` rather than vanishing between turns.
/// Money routes print the amount; subscription and local routes print the
/// same words the chip uses; a metered route that has not spent yet prints
/// `$0.00`; a session with no confirmed price says `unknown` rather than
/// inventing a zero.
#[must_use]
pub fn session_cost_label(&self) -> String {
let chip = self.cumulative_usage_chip();
if matches!(chip, codewhale_engine::route_billing::UsageChip::Hidden) {
return self
.format_cost_amount(self.displayed_session_cost_for_currency(self.cost_currency));
}
codewhale_engine::route_billing::format_usage_chip(&chip, self.ui_locale).unwrap_or_else(
|| {
self.format_cost_amount(
self.displayed_session_cost_for_currency(self.cost_currency),
codewhale_localization::tr(
self.ui_locale,
codewhale_localization::MessageId::CmdCostUnknownValue,
)
.into_owned()
},
)
}
+1 -4
View File
@@ -1590,10 +1590,7 @@ fn turn_route_lines(app: &App) -> Vec<String> {
lines.push("Cost: local".to_string());
}
codewhale_engine::route_billing::UsageChip::Unknown(_) => {
lines.push(
codewhale_engine::route_billing::format_usage_chip(&chip, app.ui_locale)
.unwrap_or_default(),
);
lines.push("Cost (session): not priced yet".to_string());
}
codewhale_engine::route_billing::UsageChip::Hidden => {}
}
+11 -15
View File
@@ -3117,9 +3117,10 @@ mod tests {
assert_eq!(super::billing_tier_label(&app, peak), None);
}
/// A provider switch must not hide missing historical coverage.
/// A provider switch must not fabricate a price: the glance carries
/// confirmed dollars, so an unpriced session renders no cost at all.
#[test]
fn cost_unknown_preserves_saved_coverage_across_route_changes() {
fn cost_shows_confirmed_dollars_only_across_route_changes() {
use codewhale_engine::route_billing::BillingPresentation;
let mut app = app_with_context_percent(10);
app.session.cost_coverage_unknown_legacy = true;
@@ -3131,39 +3132,34 @@ mod tests {
));
assert_eq!(
super::session_cost_label(&app),
"cost: unknown (saved coverage unavailable)"
"",
"an unpriced session shows no money"
);
let row = metrics_row(&app, 200);
assert!(
row.contains("cost: unknown"),
"a priceable route keeps the honesty: {row:?}"
);
assert!(!row.contains('$'), "no invented price: {row:?}");
app.billing_presentation = BillingPresentation::Unknown;
assert!(matches!(
app.cumulative_usage_chip(),
codewhale_engine::route_billing::UsageChip::Unknown(_)
));
assert_eq!(
super::session_cost_label(&app),
"cost: unknown (saved coverage unavailable)"
);
assert_eq!(super::session_cost_label(&app), "");
let ids: Vec<InfoSegmentId> = super::info_segments(&app, 200)
.iter()
.map(|segment| segment.id)
.collect();
assert!(ids.contains(&InfoSegmentId::Cost), "{ids:?}");
assert!(!ids.contains(&InfoSegmentId::Cost), "{ids:?}");
let row = metrics_row(&app, 200);
assert!(
row.contains("saved coverage unavailable"),
"an unclassified route preserves the reason: {row:?}"
!row.contains('$'),
"a route change invents nothing: {row:?}"
);
assert!(
row.contains("context 10%"),
"and nothing else moves: {row:?}"
);
// A real price on an otherwise unclassified route still prints.
// A confirmed price on an otherwise unclassified route still prints.
app.session.cost_coverage_unknown_legacy = false;
app.session.cost_priced_turns = 1;
app.session.session_cost = 0.42;
+31 -97
View File
@@ -699,10 +699,11 @@ fn statusline_full_frame_presets_preserve_transcript_composer_and_hitboxes() {
}
}
/// #5976 intentionally supersedes #5950's blanket custom-cost omission:
/// missing coverage is evidence, independent of whether today's route is known.
/// #5976 supersedes #5950's blanket custom-cost omission, and the cost line
/// now carries confirmed dollars only: an unpriced session shows no money at
/// any width, and a partly priced one shows exactly the confirmed amount.
#[test]
fn statusline_full_frame_custom_cost_preserves_evidence_and_width_shedding() {
fn statusline_cost_shows_confirmed_dollars_only() {
use codewhale_config::ProviderKind;
use codewhale_engine::route_billing::{BillingPresentation, UsageChip};
use codewhale_runtime::config::{ChromeRowPreset, StatusItem};
@@ -723,122 +724,55 @@ fn statusline_full_frame_custom_cost_preserves_evidence_and_width_shedding() {
app.posture_bar = ChromeRowPreset::Hidden;
app.metrics_line = ChromeRowPreset::Compact;
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
let expected = "cost: unknown (saved coverage unavailable)";
assert_eq!(app.api_provider, ProviderKind::Custom);
assert!(matches!(app.cumulative_usage_chip(), UsageChip::Unknown(_)));
assert_eq!(super::session_cost_label(&app), expected);
let (rows, _) = draw_into(&mut app, &mut terminal);
eprintln!("{width}x{height} custom-saved-unknown\n{}", rows.join("\n"));
let metrics = rows.last().unwrap();
assert!(metrics.contains("context 0%"), "{metrics}");
if width >= 60 {
assert!(metrics.contains(expected), "{width}: {metrics}");
} else {
// The existing whole-segment shed ladder cannot fit the reason
// plus context in 40 columns. It must not invent a zero price.
assert!(!metrics.contains("cost:"), "{metrics}");
assert!(!metrics.contains('$'), "{metrics}");
}
assert_eq!(
super::session_cost_label(&app),
expected,
"shedding changes no receipt"
"",
"{width}: an unpriced session shows no money"
);
let (rows, _) = draw_into(&mut app, &mut terminal);
let metrics = rows.last().unwrap();
assert!(metrics.contains("context 0%"), "{metrics}");
assert!(!metrics.contains('$'), "{width}: {metrics}");
assert!(!metrics.contains("cost"), "{width}: {metrics}");
app.status_items.retain(|item| *item != StatusItem::Cost);
let (hidden, _) = draw_into(&mut app, &mut terminal);
assert!(!hidden.last().unwrap().contains("cost:"));
assert_eq!(
super::session_cost_label(&app),
expected,
"a toggle changes no receipt"
hidden, rows,
"an empty cost line changes no frame at {width}"
);
app.status_items.push(StatusItem::Cost);
assert_eq!(
draw_into(&mut app, &mut terminal).0,
rows,
"live cost toggle restores the same frame"
);
// A confirmed amount with a coverage gap prints as that amount alone.
app.billing_presentation = BillingPresentation::Metered;
app.session.cost_coverage_unknown_legacy = false;
app.session.cost_priced_turns = 1;
app.session.cost_unpriced_turns = 1;
app.session.cost_unpriced_reasons.insert(
codewhale_engine::pricing::UnpricedReason::NoPricingRow
.label()
.to_string(),
app.session.session_cost = 0.42;
assert!(
matches!(
app.cumulative_usage_chip(),
UsageChip::PricedSubtotal { .. }
),
"{:?}",
app.cumulative_usage_chip()
);
let missing_rate = "cost: unknown (rate unavailable)";
assert_eq!(super::session_cost_label(&app), missing_rate);
let (unpriced, _) = draw_into(&mut app, &mut terminal);
eprintln!(
"{width}x{height} custom-unpriced-turn\n{}",
unpriced.join("\n")
let label = super::session_cost_label(&app);
assert!(
label.contains("0.42") && !label.contains("unknown"),
"{label}"
);
let (priced, _) = draw_into(&mut app, &mut terminal);
if width >= 60 {
assert!(
unpriced.last().unwrap().contains(missing_rate),
"{unpriced:?}"
);
} else {
assert!(!unpriced.last().unwrap().contains("cost:"), "{unpriced:?}");
assert!(priced.last().unwrap().contains(&label), "{priced:?}");
}
assert_eq!(app.session.cost_unpriced_turns, 1);
assert!(matches!(app.cumulative_usage_chip(), UsageChip::Unknown(_)));
// An unavailable effective effort is omitted independently of the
// explicit missing-cost reason. At 100 columns both model and reason fit.
if width == 100 {
app.status_items.insert(0, StatusItem::Model);
let (with_model, _) = draw_into(&mut app, &mut terminal);
eprintln!(
"{width}x{height} custom-model-and-unpriced-turn\n{}",
with_model.join("\n")
);
let metrics = with_model.last().unwrap();
assert!(metrics.contains("vendor-model-x"), "{metrics}");
assert!(metrics.contains(missing_rate), "{metrics}");
assert_eq!(app.provable_reasoning_effort_label(), None);
assert!(!metrics.contains("high"), "{metrics}");
assert!(!metrics.contains("effective unavailable"), "{metrics}");
app.status_items.remove(0);
}
app.session.cost_unpriced_turns = 0;
app.session.cost_unpriced_reasons.clear();
app.session.cost_priced_turns = 1;
app.session.session_cost = 0.42;
let (priced, _) = draw_into(&mut app, &mut terminal);
eprintln!(
"{width}x{height} custom-recorded-price\n{}",
priced.join("\n")
);
assert!(matches!(app.cumulative_usage_chip(), UsageChip::Money(_)));
assert!(
priced.last().unwrap().contains("0.42"),
"real fixture price survives Custom: {priced:?}"
);
app.set_provider_identity(ProviderKind::Deepseek, "deepseek");
app.active_route_base_url = "https://api.deepseek.com/v1".to_string();
app.model = "deepseek-v4-pro".to_string();
app.billing_presentation = BillingPresentation::Metered;
let (first_party, _) = draw_into(&mut app, &mut terminal);
assert_eq!(
first_party.last(),
priced.last(),
"historical price does not follow today's provider"
);
app.set_provider_identity(ProviderKind::Custom, "my-gateway");
app.active_route_base_url = "https://gateway.example/v1".to_string();
app.model = "vendor-model-x".to_string();
app.billing_presentation = BillingPresentation::Unknown;
let (restored_custom, _) = draw_into(&mut app, &mut terminal);
assert_eq!(restored_custom.last(), priced.last());
}
}
/// Feed real estimated conversation tokens through the composed frame, then
/// change only the route window. Crossing the warning threshold must repaint
/// both text and ink without moving the transcript, composer or inspector.
#[test]
fn statusline_full_frame_context_reading_updates_below_and_at_warning() {
use crate::tui::tideline::InteractionTargetId;
+1 -1
View File
@@ -1823,7 +1823,7 @@ admission rule and one safety net, and they differ in scope and trust.
| --- | --- | --- | --- |
| `POST /v1/snapshots/{id}/restore` | whole server workspace, or the `thread_id` workspace | bearer token only (operator action) | no |
| `POST /v1/threads/{id}/patch-undo` | the files the dropped turns changed | thread `trust_mode` or `auto_approve` when files would change | yes |
| `POST /v1/threads/{id}/file-revert` | exactly one regular file | thread `trust_mode` or `auto_approve`, always | no |
| `POST /v1/threads/{id}/file-revert` | exactly one regular file | none; the restore point must be one the thread owns and `expected_hash` must match | no |
**Admission.** A restore reserves the same admission the Runtime uses for
config reloads and session checkpoints, so no new turn starts and no saved