Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions devlog/_plan/260907_lane_d/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Lane D release-train roadmap

Satisfy-spec HOTL for delegated recommendations #16 → #17 → #15 → #21 → #25.
Goal: independently audited manual dependent PRs ready for main-session integration.
Scope: Claude outbound, display-name dialog, usage costs/overlays/summary, usage GUI,
plus directly required CLI/API/tests/docs. i18n files are append-only shared per main's
2026-09-07 correction. No other lane-owned files; no merge/release/main/preview.
No local test/typecheck/build/install. Remote ci.yml lane=all at final top SHA is
sole product verifier. Local source and diff checks are not execution evidence.
No user token or wall-clock bound supplied. Use existing repo/GitHub authorization.
Stop: top-head green with reviewer verdicts and layer PR/SHA evidence; otherwise
record exact DEFER/BLOCKED reasons without claiming implementation passes.
Memory/evidence: this unit plus .tmp/lane-d for review drafts. Unpublished security
material stays in scratch. Reclaim failed delegated work after two distinct agents;
other-lane file collision requires main coordination.

## Dependency and publication map

| Phase | Item | Outcome | Branch |
|---|---|---|---|
| 0 | Roadmap | Lock all diff plans before code | first layer docs |
| 1 | #3719 slice | Legacy redacted-before-signed SSE/JSON parity | codex/260907-d1-thinking |
| 2 | receipt guard | Prevent new intent while recovery is pending | codex/260907-d2-receipt |
| 3 | #3817 | Exact account identity resolves provider overlays | codex/260907-d3-account-prices |
| 4 | #3667 | Price editor + CLI + authoritative explicit zero | codex/260907-d4-price-editor |
| 5 | #3379 slice / #2956 | Inclusive custom usage bounds + GUI | codex/260907-d5-usage-ranges |
| 6 | readiness | Fresh top CI, screenshots and implementation audits | top branch |

All lower subjects include [skip ci]; every push uses --no-verify. Native stack null.
Only phase 6 dispatches ci.yml lane=all; failures get Astra-high exact-log diagnosis,
fixes on their owning layer and rebase --update-refs cascade. Main alone merges.
#3719 and #3379 stay open. #2956 credit uses verified GitHub author identity.
16 changes: 16 additions & 0 deletions devlog/_plan/260907_lane_d/001_roadmap_audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Roadmap audit resolution

Astra Herschel (01a07b2b-5148-73c0-a067-a13485ab32c9) returned
GO-WITH-FIXES with four bounded roadmap corrections. All are incorporated in
040_price_editor.md and 050_usage_ranges.md: register management routes; persist
manual-price display state; filter individual ledger entries before daily aggregation;
preserve apiKeyId and scan consistency; define milliseconds and explicit window bounds.

Astra Dirac identified two thinking design blockers, recorded in 010 for re-audit:
item ownership and simultaneous reasoning/frame retention. Astra Ohm limits the account
mapping to evidenced Codex identities and requires consistent tier-namespace resolution.
The first implementation phase must finish those fold-backs before code changes.

Only documentation has changed. Source references were inspected; product tests,
typecheck, builds and installs are NOT RUN by delegation instruction. Product acceptance
remains open until top-head Cross-platform CI executes lane=all.
26 changes: 26 additions & 0 deletions devlog/_plan/260907_lane_d/010_thinking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 010 Thinking ordering
MODIFY src/claude/outbound.ts ensureBlock/closeOpenBlock and reasoning done.
Before: thinking start/deltas are emitted immediately; done closes thinking then red.
After: retain already-budgeted thinking text, defer its start/index/delta until close;
reasoning done emits red blocks before flushing pending signed thinking. Preserve text
and tool order, hidden env.txt non-disclosure, genuine signature and budget release.
MODIFY tests/claude-integration/claude-outbound.test.ts: compare collected SSE against
literal expected content and JSON for combined envelopes with preceding deltas,
multiple summary parts/red blocks, text prefix, signed-only, red-only. Check sequential
non-overlapping block indices and cancellation/overflow existing assertions.
Independent Astra audit must resolve streaming latency and allocation implications.

Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI.

## A audit fold-back
Astra Dirac found two blockers: unmatched-item reordering and closure memory overlap.
Track bounded reasoningItemKey separately from part identity; flush on changed explicit
item identity, and close unrelated pending thinking before another item's red blocks.
Only same identity (including both omitted) reorders red before pending thinking.
Retain thinkingBuf through signature emission as before;
queued frame budget stays authoritative, never weakened. Add near-limit valid control,
shared-budget collector control, overflow/cancel regressions. Deferred thinking is an
accepted visible-latency tradeoff; text/tool frames remain live with incremental-reader
coverage. Late done after a different emitted block cannot reorder earlier content.

Re-audit Dirac: VERDICT PASS, blockers=0. Accept tight artificial budget capacity reduction; retain original overflow assertions and production limits.
17 changes: 17 additions & 0 deletions devlog/_plan/260907_lane_d/020_receipt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 020 Display-name receipt recovery
MODIFY gui/src/components/ModelDisplayNameDialog.tsx.
Before: input/reset enabled whenever saving=false; input onEdit clears recovery.
After: new mutationOutcomeUnknown prop from Models.tsx recovery.confirmed===false
disables draft editing and reset, submit retains
read/retry action. Handler guards prevent synthetic events bypassing disabled controls.
Close/cancel stays available. This is bounded UI recovery, not server request ordering.
MODIFY gui/tests/models-display-name-editor.test.tsx: unknown receipt cannot replace intent; retry recovers; confirmed saved:true
and ordinary validation error remain
editable. Screenshot changed disabled input/reset with retry available.

Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI.

Implementation: unknown outcome guards input/reset handlers and submit, and focuses Retry
when saving fails without a receipt. Saved:true remains editable. Transport/body failure
matrix attempts a replacement intent and asserts no second PUT before read-only retry.
Astra Herschel plan verdict PASS. Screenshots and product execution await top CI artifact.
13 changes: 13 additions & 0 deletions devlog/_plan/260907_lane_d/030_account_prices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 030 Account price identity
MODIFY src/usage/user-cost-overlays.ts registry refresh and signature/version.
Before: configured provider set and overlay rows only.
After: exact account identifiers/log labels from config mapped to established provider
identity. Include mapping in signature for memo and aggregate cache invalidation.
MODIFY src/usage/cost.ts resolveMatchedPrice: exact configured namespace and exact
user overlay precede account identity; unresolved suffix is never guessed/stripped.
MODIFY tests/usage/usage-cost.test.ts or existing provider-overlay tests: custom account
id, qualified id, stable log label, configured collision, unrelated hyphenated provider,
account rename/removal invalidation. Account aliases never become identity authority.
Audit determines precise supported historical labels from actual producer evidence.

Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI.
23 changes: 23 additions & 0 deletions devlog/_plan/260907_lane_d/040_price_editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 040 Manual price editor
MODIFY src/usage/cost.ts userOverlayMatch: valid operator all-zero row returns user
price, while generated catalog zeros keep unknown/fallback semantics.
MODIFY src/server/management/model-routes.ts: exact-provider model-costs GET/PUT,
validate four finite nonnegative bounded rates or null reset, preserve siblings,
rollback on persist failure, no routing/catalog mutation required for price-only edits.
MODIFY src/cli/models-runtime.ts, models-runtime-subcommands.ts and capabilities.ts:
models set-price provider/model --input N --output N [--cache-read N --cache-write N]
or --auto. GET for show and PUT for set/reset through existing management client.
ADD gui/src/components/ModelPriceDialog.tsx; MODIFY Models.tsx and models-shared.ts
only as needed: edit action, load exact saved override, inputs 4 rates USD/1M,
save/reset and manual indicator. Reuse dialog/fetch/i18n patterns. All locale keys
append-only pricing.override.*. Add endpoint, CLI, estimator and GUI regressions;
register new test files in both append-only layout manifests. Public docs and generated
CLI surface map mirror actual capability entries; source-generation commands NOT RUN
locally so map is updated by its source contract without claiming verification.

Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI.

A fold-back: add GET/PUT entries in src/server/management/route-registry.ts.
Reuse providerModelCostsConfigError. GET returns sanitized per-provider modelCosts map;
Models owns a typed map loaded with catalog or dedicated GET, so manual badges survive
reload. CLI omitted cache-read/cache-write rates default to zero, explicitly documented.
30 changes: 30 additions & 0 deletions devlog/_plan/260907_lane_d/050_usage_ranges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 050 Custom usage windows
REIMPLEMENT range slice from PR #2956 with Manson2438 credit; do not carry offline reports.
ADD src/usage/time-range.ts strict timestamp parser and inclusive since/until bounds;
MODIFY summary.ts accumulator interface to support bounded windows without poisoning
preset daily aggregates. Use stream ledger filtering for partial days if compact daily
partitions cannot answer exact boundaries. Reject malformed/reversed bounds at API/CLI.
MODIFY src/server/management/logs-usage-routes.ts custom-window path before preset cache,
stream/filter into isolated accumulator preserving surface/provider/model and truncation
metadata. Do not persist normalized ledger rows. Include bounds in response.
MODIFY CLI observe/capabilities usage flags and GUI Usage.tsx custom datetime inputs,
independent draft/applied bounds, cache key includes bounds, grid anchored to effective
window, clear returns to preset. All locale keys append-only usage.range.*.
Tests: inclusive boundaries, partial same-day, reversed/invalid, empty ledger, existing
provider/model/surface filters, preset cache after custom query; GUI apply/clear/errors.
Public API/CLI docs describe epoch/ISO contract and local datetime conversion.

Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI.

A fold-back: immutable window option on createUsageSummaryAccumulator; add() checks
inclusive bounds AFTER recording whole-scan snapshot timestamps but BEFORE partitioning.
clone preserves window. summarize uses window endpoint for grid, actual now for generatedAt;
retain 366-day grid cap. Custom queries use isolated row-unique accumulator via existing
getFilteredUsageAggregate with window in key. Reuse overlay/timezone revision restart
and scanner identity controls. Preserve apiKeyId and current filter echo alongside all
other filters. USAGE_RANGES remains preset-only; response range stays selected preset
with customWindow:true, since/until explicit bounds (bounds override preset). API accepts
integer epoch milliseconds or full ISO-8601 with timezone only; require both bounds;
reject negative/unsafe/date-invalid/reversed, never normalize overflow dates.
MODIFY src/cli/usage-report.ts heading prints since/until for customWindow responses.
GUI datetime values become epoch ms locally; end selected minute includes 59.999s.
20 changes: 14 additions & 6 deletions gui/src/components/ModelDisplayNameDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ModelDisplayNameDialogProps {
saving: boolean;
requestError: string | null;
currentNamePending?: boolean;
mutationOutcomeUnknown?: boolean;
onRetry?: () => void;
onEdit?: () => void;
onSave: (displayName: string) => void;
Expand All @@ -28,6 +29,7 @@ export default function ModelDisplayNameDialog({
saving,
requestError,
currentNamePending = false,
mutationOutcomeUnknown = false,
onRetry,
onEdit,
onSave,
Expand All @@ -37,6 +39,7 @@ export default function ModelDisplayNameDialog({
const t = useT();
const dialogRef = useRef<HTMLDialogElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const submitRef = useRef<HTMLButtonElement>(null);
const wasSavingRef = useRef(saving);
const titleId = useId();
const helpId = useId();
Expand All @@ -55,8 +58,11 @@ export default function ModelDisplayNameDialog({
useEffect(() => {
const saveFailed = wasSavingRef.current && !saving && Boolean(requestError);
wasSavingRef.current = saving;
if (saveFailed) inputRef.current?.focus();
}, [requestError, saving]);
if (saveFailed) {
if (mutationOutcomeUnknown) submitRef.current?.focus();
else inputRef.current?.focus();
}
}, [requestError, saving, mutationOutcomeUnknown]);

// Parent replaces this snapshot only after a confirmed mutation, not typing or polling.
// Adjust before committing children, preserving the mounted dialog and its focus refs.
Expand Down Expand Up @@ -102,6 +108,7 @@ export default function ModelDisplayNameDialog({
event.preventDefault();
if (saving) return;
if (onRetry) { onRetry(); return; }
if (mutationOutcomeUnknown) return;
const nextValidationKey = modelDisplayNameValidationKey(draft);
setValidationKey(nextValidationKey);
if (!nextValidationKey) onSave(draft.trim());
Expand Down Expand Up @@ -137,8 +144,9 @@ export default function ModelDisplayNameDialog({
placeholder={t("models.displayNamePlaceholder")}
aria-describedby={`${helpId}${visibleError ? ` ${errorId}` : ""}`}
aria-invalid={validationError ? true : undefined}
disabled={saving}
disabled={saving || mutationOutcomeUnknown}
onChange={event => {
if (saving || mutationOutcomeUnknown) return;
onEdit?.();
setDraft(event.target.value);
setValidationKey(null);
Expand All @@ -157,15 +165,15 @@ export default function ModelDisplayNameDialog({
<button
type="button"
className="btn btn-ghost btn-sm"
disabled={saving || !model.displayNameOverride}
onClick={onReset}
disabled={saving || mutationOutcomeUnknown || !model.displayNameOverride}
onClick={() => { if (!saving && !mutationOutcomeUnknown) onReset(); }}
>
{t("models.displayNameReset")}
</button>
<button type="button" className="btn btn-sm" disabled={saving} onClick={requestClose}>
{t("common.cancel")}
</button>
<button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
<button ref={submitRef} type="submit" className="btn btn-primary btn-sm" disabled={saving || (mutationOutcomeUnknown && !onRetry)}>
{saving ? t("common.saving") : onRetry ? t("common.retry") : t("common.save")}
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions gui/src/pages/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2643,6 +2643,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string;
saving={displayNameSaving}
requestError={displayNameRequestError}
currentNamePending={displayNameCurrentPending}
mutationOutcomeUnknown={displayNameRecovery?.confirmed === false}
onRetry={displayNameRecovery ? () => void saveDisplayName(displayNameRecovery.value) : undefined}
onEdit={() => setDisplayNameRecovery(null)}
onSave={value => void saveDisplayName(value)}
Expand Down
12 changes: 10 additions & 2 deletions gui/tests/models-display-name-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,16 @@ describe("Models dashboard discovered display name integration", () => {
expect(currentNameText()).toContain("Current name unavailable until refresh");
expect(currentNameText()).not.toContain("Your name");
expect(container.textContent).toContain("The change may have been saved");
expect(dialogInput().disabled).toBe(true);
expect(dialogButton("Reset name").disabled).toBe(true);
expect(dialogButton("Retry").disabled).toBe(false);
expect(dialogButton("Cancel").disabled).toBe(false);
await act(async () => {
setInputValue(dialogInput(), "Replacement intent");
dialogButton("Reset name").dispatchEvent(new testWindow.MouseEvent("click", { bubbles: true }));
});
expect(dialogButton("Retry").disabled).toBe(false);
expect(mutationBodies).toHaveLength(1);
await act(async () => container.querySelector("dialog form")!.dispatchEvent(
new testWindow.Event("submit", { bubbles: true, cancelable: true }),
));
Expand Down Expand Up @@ -522,12 +530,12 @@ describe("Models dashboard discovered display name integration", () => {
if (stage === "reload") expect(seenSignals[1]).toBe(seenSignals[0]);
await act(async () => deadline.abort(new DOMException("Timed out", "TimeoutError")));
await flush();
expect(dialogInput().disabled).toBe(false);
expect(dialogInput().disabled).toBe(stage === "mutation");
expect(dialogButton("Cancel").disabled).toBe(false);
expect(dialogInput().value).toBe("Possibly saved");
expect(container.textContent).toContain(stage === "mutation"
? "The change may have been saved" : "The change was saved");
expect(testWindow.document.activeElement).toBe(dialogInput());
expect(testWindow.document.activeElement).toBe(stage === "mutation" ? dialogButton("Retry") : dialogInput());
stall = false;
if (descriptor) Object.defineProperty(AbortSignal, "timeout", descriptor);
await act(async () => dialogButton("Retry").click());
Expand Down
Loading
Loading