Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5cec0a3
test(gui): guard the properties the Aside and Integrations work relie…
lidge-jun Aug 31, 2026
71bd7be
chore(release): move dev to 2.39.0 after the v2.38.0 release (#3076)
lidge-jun Aug 31, 2026
873d08e
docs(devlog): plan the remaining marks, page-wide logos and conflict …
lidge-jun Aug 31, 2026
44b4de3
feat(gui): give the last three export clients their real marks (#3082)
lidge-jun Aug 31, 2026
d86ec3e
feat(gui): put the client's logo on every Integrations surface (#3083)
lidge-jun Aug 31, 2026
2a90cda
feat(integrations): let the user overwrite a conflicted config on pur…
lidge-jun Aug 31, 2026
06d6880
docs(devlog): close the Aside + Integrations unit with what the plan …
lidge-jun Aug 31, 2026
0cc7341
fix(gui): stop the Grok mark disappearing on the dark theme (#3086)
lidge-jun Aug 31, 2026
3982bb7
docs(devlog): scan the priority-70 backlog for round 2 and lock the r…
lidge-jun Aug 31, 2026
b4303bb
docs(devlog): record the wp6/wp5/wp4 entitlement stack outcome (#3079)
lidge-jun Aug 31, 2026
91b2c4e
feat(cli): let the terminal resolve a conflict too (#3088)
lidge-jun Aug 31, 2026
6e3abe8
docs(devlog): record the two defects the audit found after the unit c…
lidge-jun Aug 31, 2026
5ef84b6
docs(devlog): plan brand marks for the 38 providers that render bare …
lidge-jun Aug 31, 2026
a0d386b
fix(responses): always carry query on web_search_call for Console Go …
lidge-jun Aug 31, 2026
a11038c
fix(gui): wire the provider marks that were already committed (#3093)
lidge-jun Aug 31, 2026
dcbc280
fix(gui): give every provider card the same control row (#3096)
lidge-jun Aug 31, 2026
910b4c7
feat(gui): source brand marks for 23 providers that rendered a letter…
lidge-jun Aug 31, 2026
5f0b390
fix(responses): bound the durable spill directory with an aggregate b…
lidge-jun Aug 31, 2026
15f92e3
fix(gui): make every provider mark survive both themes (#3098)
lidge-jun Aug 31, 2026
d71aa07
feat(gui): give the Add-Provider catalog its logos (#3099)
lidge-jun Aug 31, 2026
9d122dd
docs(devlog): close the provider-marks unit with what the plan got wr…
lidge-jun Aug 31, 2026
6123be3
fix(history): resolve a rollout's session_meta by thread id (#3026) (…
lidge-jun Aug 31, 2026
d98d22c
fix(oauth): clear the refresh intent after a transient Anthropic failure
luvs01 Aug 30, 2026
dd7b6ab
fix(oauth): preserve Anthropic intent across uncertain refresh outcomes
luvs01 Aug 30, 2026
8728c0f
fix(oauth): resume retry-safe Anthropic intent cleanup
luvs01 Aug 30, 2026
e476acd
fix(oauth): keep post-commit intent cleanup from masking a durable cr…
luvs01 Aug 30, 2026
ab2f716
test(oauth): use declared cleanup operation
luvs01 Aug 30, 2026
5bb4113
test(oauth): preserve refresh-intent spy hooks
luvs01 Aug 30, 2026
a41b799
fix(oauth): adopt newer disk credentials before cleanup
luvs01 Aug 30, 2026
c418819
fix(oauth): retry refresh intent marker contention
luvs01 Aug 31, 2026
42ad9c4
fix(routing): treat a live full burst window as exhausted, not unknow…
lidge-jun Aug 31, 2026
330470e
fix(stop): typed stop outcome so a history-only failure does not abor…
lidge-jun Aug 31, 2026
b46164e
fix(catalog): widen the dated-variant fold and guard its direction at…
lidge-jun Sep 1, 2026
a3656a9
fix(cursor): retry pre-header discovery eof (#3102)
lidge-jun Sep 1, 2026
0ef04e6
fix(cli): stop start shadowing a live configured-port proxy, and retr…
lidge-jun Sep 1, 2026
e9d198a
fix(responses): strip Codex-private item metadata at the noncanonical…
lidge-jun Sep 1, 2026
2869ec0
Merge pull request #3111 from lidge-jun/codex/2989-anthropic-refresh-…
lidge-jun Sep 1, 2026
5c0c131
fix(responses): notice an unreadable MESSAGE reply, not just NEW_TASK…
lidge-jun Sep 1, 2026
a73a4c9
fix(codex): refresh a stored token before quarantining it on a WHAM 4…
lidge-jun Sep 1, 2026
6d03687
docs(devlog): rescan the bug backlog after wp5 and wp6 (#3120)
lidge-jun Sep 1, 2026
b6e53d8
fix(gui): give restore focus somewhere to land when its trigger is go…
lidge-jun Sep 1, 2026
0db8066
feat(gui): let Logs be filtered by model or provider (#3115)
lidge-jun Sep 1, 2026
9af3a7b
fix(catalog): advertise image input for modalities-declared sidecar r…
lidge-jun Sep 1, 2026
682112e
Merge remote-tracking branch 'origin/dev' into codex/promote-main-2390
lidge-jun Sep 1, 2026
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
Binary file added .github/pr-assets/260901-cap-slot-ko-1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 58 additions & 6 deletions bin/ocx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
* src/cli/index.ts — only the published npm `bin` routes through here.)
*/
import { spawn, spawnSync } from "node:child_process";
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../src/update/stop-contract.mjs";
import { probeProxyLiveness } from "../src/update/proxy-liveness-probe.mjs";
import { decidePostStopUpdate } from "../src/update/stop-decision.mjs";
import { randomBytes } from "node:crypto";
import { createRequire } from "node:module";
import { existsSync, readFileSync, readdirSync } from "node:fs";
Expand All @@ -17,6 +20,7 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";
import { npmInvocation } from "../src/update/npm-invocation.mjs";
import { hasPendingTeardownIn } from "../src/config/pending-teardown-names.mjs";
import {
npmCachePreflightFailureMessage,
runNpmCachePreflight,
Expand Down Expand Up @@ -202,7 +206,12 @@ function runNpmSelfUpdate() {
// Capture listen target before stop clears runtime-port.json (mirrors GUI/CLI update worker).
// Do not treat a live runtime port of 10100 as "missing" — track whether the read succeeded.
let bakePort = 10100;
// The hostname travels with the port: a proxy bound to ::1 or a specific interface is
// invisible to a probe that assumes 127.0.0.1, and "no answer" would then read as
// "stopped" for exactly the proxy the probe exists to find.
let bakeHostname = "127.0.0.1";
let sawRuntimePort = false;
let sawRuntimeHostname = false;
try {
const rt = JSON.parse(readFileSync(join(configDir(), "runtime-port.json"), "utf8"));
if (Number.isFinite(rt?.port) && rt.port > 0 && rt.port <= 65535) {
Expand All @@ -219,16 +228,29 @@ function runNpmSelfUpdate() {
}
if (runtimeLive) {
bakePort = Math.trunc(rt.port);
if (typeof rt?.hostname === "string" && rt.hostname.trim() !== "") {
bakeHostname = rt.hostname.trim();
sawRuntimeHostname = true;
}
sawRuntimePort = true;
}
}
} catch { /* fall through to config */ }
if (!sawRuntimePort) {
// Port and hostname resolve INDEPENDENTLY: a legacy runtime record carries a port and no
// hostname, and skipping config in that case probed 127.0.0.1 for a proxy bound to ::1.
if (!sawRuntimePort || bakeHostname === "127.0.0.1") {
try {
const cfg = JSON.parse(readFileSync(join(configDir(), "config.json"), "utf8"));
if (Number.isFinite(cfg?.port) && cfg.port > 0 && cfg.port <= 65535) bakePort = Math.trunc(cfg.port);
if (!sawRuntimePort && Number.isFinite(cfg?.port) && cfg.port > 0 && cfg.port <= 65535) {
bakePort = Math.trunc(cfg.port);
}
if (!sawRuntimeHostname && typeof cfg?.hostname === "string" && cfg.hostname.trim() !== "") {
bakeHostname = cfg.hostname.trim();
}
} catch { /* keep default */ }
}
// Wildcard and bracketed-IPv6 normalization lives in probeProxyLiveness, so both lanes
// get it from one place.

const launcher = fileURLToPath(import.meta.url);

Expand Down Expand Up @@ -343,17 +365,47 @@ function runNpmSelfUpdate() {
}
}

if (serviceWasInstalled || hasRuntimeState) {
// An outstanding pending-teardown receipt is a fourth reason to run the stop. After a
// parent crashed mid-deferral the service, pid and runtime records can all be absent
// while the shared client config still points at a proxy that is gone; installing over
// that silently skips the recovery the receipt was written to trigger (#3008). Presence
// is the whole test here — the launcher cannot parse it, and `ocx stop` is what decides
// whether the obligation is safe to finish.
const hasPendingTeardown = hasPendingTeardownIn(readdirSync, configDir());
if (serviceWasInstalled || hasRuntimeState || hasPendingTeardown) {
console.log("⏹ Stopping the running proxy before updating...");
const stopRes = spawnSync(process.execPath, [launcher, "stop"], { stdio: "inherit", windowsHide: true });
const stillHasRuntimeState =
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
if (stopRes.status !== 0 || stillHasRuntimeState) {
// A history-only failure means teardown succeeded and a backup manifest is waiting for
// review: the proxy is down and replacing package files is safe. Every other nonzero
// status is a stop that did not finish, and a signal kill (status null) says nothing
// about whether it did - both abort, because replacing files under a live server
// leaves it running mixed old and new modules (#3008).
// The same decision the Bun updater makes, from the same module (#3008). Absent PID and
// runtime files are weak evidence, so the captured endpoint is asked; "unknown" aborts
// because a silent listener is exactly the state where replacing files is dangerous.
const decision = decidePostStopUpdate({
status: stopRes.status,
hasRuntimeState: stillHasRuntimeState,
// Re-checked AFTER the stop: a quarantined receipt lets the stop itself succeed
// (there is nothing left to stop), so a pre-stop check alone let the retry install
// over a teardown that never ran.
teardownOutstanding: hasPendingTeardownIn(readdirSync, configDir()),
liveness: probeProxyLiveness(bakePort, bakeHostname),
});
const historyOnlyStop = decision.reason === "history-only";
if (!decision.proceed) {
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
console.error("opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
if (decision.reason === "teardown-outstanding") {
console.error("opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.");
console.error("opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.");
} else console.error(decision.reason === "proxy-unknown"
? `opencodex: could not confirm the proxy on ${bakeHostname}:${bakePort} is stopped; aborting the update. Run 'ocx stop' and retry.`
: "opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
process.exit(1);
}
if (historyRestoreIncomplete()) {
if (historyOnlyStop || historyRestoreIncomplete()) {
console.warn(
"opencodex: WARNING — Codex resume-history metadata restore is incomplete (a backup manifest remains).\n" +
" The DB may be busy or the manifest/target may need review; untracked routed history is intentionally unchanged.\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ per-row borders are literally what produces that texture.
| wp4 | 030 | Rollback surface redesign |
| wp5 | 040 | Brand marks for the nine clients showing a monogram |
| wp6 | 050 | Stacked PR chain |
| wp7 | 060 | Marks for the last three monogram clients |
| wp8 | 070 | Marks on every Integrations surface, not just the API rows |
| wp9 | 080 | Conflict overwrite: writer, route, GUI dialog |

wp7 through wp9 were appended after the original six. Outcome and corrections:
`090_outcome.md`.

Research docs: 001 (Aside contract), 002 (registration checklist),
003 (Integrations UX diagnosis), 004 (brand mark provenance).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# The three clients still on a monogram

Continuation of `004_brand_mark_provenance.md`, which closed six clients. After
that pass `CLIENT_MARKS` covers nine of twelve; `hermes`, `gajae` and `mcode`
still render `label.slice(0, 1)`.

004 recorded `gajae` and `hermes` as monogram-only because neither publishes an
SVG with path geometry. That verdict stands on its own terms and is now
superseded by a wider rule: raster-to-vector conversion is authorized, so "the
vendor ships no SVG" no longer ends the search. Every mark below is traced from
the product's own raster asset rather than redrawn.

All three were located 2026-08-31 through the `aside-jun` skill driving a
signed-in Aside browser, then re-fetched and verified locally.

## mcode — MiniMax Code

A genuine first-party SVG exists and 004 simply had not found it.

- Source: `https://raw.githubusercontent.com/MiniMax-AI/MiniMax-01/main/figures/minimax.svg`
- 1255 bytes, `viewBox="0 0 490.16 411.7"`, one `<path>` filled by a three-stop
linear gradient (`#e4177f` to `#e73562` to `#e94e4a`).
- It is the standalone symbol — the interlocking wave glyph with no wordmark
beside it. The docs-site asset (`mintcdn.com/minimax-zh/.../logo/light.svg`)
is the 129x32 horizontal lockup and was rejected for that reason: a wordmark
in a 20px square renders as unreadable letter mush.
- Publisher mark rather than product mark. MiniMax Code ships no mark of its
own and MiniMax is its publisher, so this is the closest first-party asset.
- Committed unmodified apart from dropping the Chinese-language `<title>` and
layer-name metadata the authoring tool left behind. The gradient id is
renamed: `未命名的渐变_6` means "unnamed gradient 6", it collides across
inlined documents, and a non-ASCII id in a shared namespace is a trap.
- Multi-color, so it must NOT enter `MONOCHROME_CLIENT_MARKS`: masking would
flatten the gradient to one ink.

## hermes — Hermes agent

No usable SVG upstream; traced from the product's own application icon.

- Rejected first: `website/static/img/favicon.svg` is 113 bytes and its whole
body is one `<text>` element. 004 already recorded this.
- Rejected second: `https://nousresearch.com/safari-pinned-tab.svg` (12746
bytes, potrace output). Its first path is `M40 2560 l0 -2560 2520 0 2520 0 0
2560 0 2560 -2520 0 -2520 0 0 -2560z` — the full 512-unit frame. Rendered at
20px that is a black square with a hairline hole, which is worse than a
monogram.
- Accepted: `apps/desktop/assets/icon.png` from `NousResearch/hermes-agent`,
574273 bytes, 1024x1024 RGBA, artwork bounded at (101,108)-(924,914). This is
the icon the Hermes desktop application ships, so it is the product's own
mark, not the publisher's.
- Quantizing the opaque pixels shows two inks: a light plate (340877 px) and
black art (241765 px), with ~20k px of antialiasing between them. It is a
single-ink illustration on a rounded plate.
- Traced with `potrace -s --flat --turdsize 8 --alphamax 1.0 --opttolerance
0.2` over the mask `alpha > 128 AND mean(rgb) < 110`, which keeps the black
art and discards the plate. One path, squared to `viewBox="0 0 823 823"` by
centering the 823x806 trace.
- `fill="currentColor"`, and it MUST join `MONOCHROME_CLIENT_MARKS`. A 20px
render on `#0d1117` confirmed the untinted mark is invisible in dark mode —
the same failure `prime`, `opencode` and `kimi` already have.

## gajae — Gajae Code

No SVG anywhere upstream, confirmed twice; traced from the mascot.

- Searched and found empty: `assets/`, `public/` (404), `docs/`, plus
`assets/logo.svg`, `assets/favicon.svg`, `public/logo.svg`,
`public/favicon.svg`, `docs/logo.svg` (all 404), and every published
`@gajae-code/*` npm tarball at 0.15.6 (no SVG entries). `docs/brand-assets.md`
lists the active marks as PNG only.
- Accepted source: `assets/character.png`, 3190496 bytes, 1550x2048 RGBA,
transparent background.
- It is a vertical lockup: the mascot occupies y < 1650 and the `gajae-code`
wordmark sits below it. Rows 1650-1682 are fully transparent, which is the
seam the crop uses. Only the mascot is traced; a wordmark would not survive
20px.
- The artwork is upscaled pixel art, so tracing at source resolution follows
every staircase and produced a 1.3 MB SVG. Downsampling to a 128px box with
Lanczos plus a 0.6px Gaussian first, then tracing, gives ~31 KB. That is
larger than any existing mark (`zcode.svg`, 11037 bytes) because this one is
an illustration rather than a glyph.
- Seven color layers, k-means++ seeded at 3 for determinism, painted
largest-area first. The committed file's fills are `#1d0a04`, `#561203`,
`#981001`, `#e3770c`, `#d32e02`, `#8f3a04` and `#02ac61` — read off
`gajae-code.svg` rather than off an earlier tuning run, whose centers differed
because it quantized at a different target size. The smallest layer is the
visor green and a fixed area floor would have dropped it, so the floor is a
fraction of the opaque area instead.
- Multi-color, so NOT in `MONOCHROME_CLIENT_MARKS`.

## Rule this pass establishes

A mark may be traced from the product's own raster asset when no vector exists,
provided the trace follows the source pixels rather than redrawing them, the
conversion parameters are recorded, and the result is verified by rendering at
the size it will actually be used. Tracing a wordmark into a square slot is
still refused, and so is a full-frame silhouette plate.
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,45 @@ And `integrations.catalog.title` -- the new `h3` that fixes the overview's headi
outline -- reads "Clients" in both English and French, which the French
accidental-English guard is right to flag. It is on the intentional-English
allowlist now.

## Outcome

Seven PRs on `dev`: #3047 (`8c1294828`) the Aside client, #3050 (`efa2ba5ad`) the
bounded rollback surface, #3049 (`704d0d91a`) the first-party marks, #3048
(`93b7ee80a`) the Aside GUI surface, #3065 (`7853e8e05`) Aside's own mark and the
single-ink fix, #3060 (`a1c332e9a`) the MAINTAINERS correction, and #3074 the
regression guards. Issue #3059 tracks the one deferral.

### What the pre-merge audit changed

An adversarial reviewer ran two rounds against the stack and neither round was
ceremonial. Round 1 found that squash-merging the parent would strand the child:
the repo merges by squash, so `git rebase` of the child onto the new `dev`
conflicts add/add, and `enforce-pr-target` resolves `stackedBase` from **open**
PRs only, so #3048 flips to `wrong_base` the moment its parent lands and its
branch is deleted. The fix is `git rebase --onto <new-dev> <recorded-parent-tip>`,
with the tip captured *before* the merge because `delete_branch_on_merge` removes
the name.

Round 2 caught the more dangerous one. A fix had landed on the parent after the
child was cut, so the child was *behind* its own base while touching the same two
files. Replaying it would have auto-merged cleanly and silently reverted the
guard. The bad outcome there is not a conflict you resolve; it is the clean
rebase you do not look at. Cascade first, then `--onto`, and verify by grepping
for the guard rather than trusting the exit code.

Both are properties of *this* repository's settings, not general stacking advice.

### The claim that was wrong

`aside` was recorded here as having no first-party mark, on the evidence that
`aside.com/favicon.svg` is a 404. True of the web, wrong about the product: the
installed application ships the mark in a module the vendor named
`official-brand-symbol`. The lookup had been scoped to what a vendor publishes
for the web, and an app that ships no web assets falls through it.

Rendering all nine marks at 28px against both themes then showed three were
invisible to somebody -- `prime` white-on-transparent in light mode, `opencode`
and `kimi` near-black in dark. Every existing assertion passed: the files
existed, the `src` values were right, the geometry check was satisfied. Only
looking at them found it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# wp7 — the three remaining marks

Depends on nothing in this unit that is not already merged. Provenance and
conversion parameters are in 005; this document is the diff.

## New assets

`gui/public/provider-icons/minimax.svg` — the MiniMax symbol, fetched not
traced. Two edits to the upstream file: the `<title>资源 2</title>` and the
`data-name` layer wrappers go (authoring-tool residue), and the gradient id
becomes `minimax-wave` so it cannot collide and carries no non-ASCII.

`gui/public/provider-icons/hermes-agent.svg` — one `currentColor` path,
`viewBox="0 0 823 823"`. Named `hermes-agent` rather than `hermes` because
Hermes is also a provider name in this repo and `provider-icons/` is one flat
namespace.

`gui/public/provider-icons/gajae-code.svg` — seven fill layers, largest area
first, `viewBox="0 0 128 128"`-class square box produced by centering the
traced bounds.

## gui/public/provider-icons/README.md

The "Two export clients deliberately have NO mark" section is now false and is
replaced. `gajae` and `hermes` move into the provenance list with their trace
parameters; `mcode` joins them. The section that remains explains the tracing
rule from 005 — a raster may be traced, a wordmark may not be squeezed into a
square slot, and a full-frame silhouette is rejected.

## gui/src/components/apikeys-workspace/client-config-clients.ts

`CLIENT_MARKS` gains three entries:

```ts
hermes: "/provider-icons/hermes-agent.svg",
gajae: "/provider-icons/gajae-code.svg",
mcode: "/provider-icons/minimax.svg",
```

`MONOCHROME_CLIENT_MARKS` gains `hermes` only. `gajae` is seven inks and
`mcode` is a gradient; masking either would flatten it.

The block comment above `CLIENT_MARKS` currently says two clients are absent on
purpose. That is now wrong in a way a reader would trust, so it is rewritten to
state the tracing rule and that every client has a mark.

## Tests

`gui/tests/client-marks-assets.test.ts` already covers more than the plan
originally credited it with. It asserts file existence, README provenance, the
no-`<text>`/no-`<image>`/must-have-geometry rule, that no multi-color mark is
masked, that the four known-invisible marks ARE masked, and that `dsh` is not.
Every one of those extends to the three new files without an edit, so the
"new guard: no `<text>` element" the plan proposed would have been a duplicate
of an existing test rather than new coverage.

What is genuinely uncovered is the completeness of the map. Nothing asserts that
every id in `CLIENTS` has a mark, so an entry dropped in a merge degrades to a
monogram silently and looks identical to a client that never had one. That guard
is new, and it was driven red by removing the `mcode` entry.

Second new guard: a traced mark must record its raster source and its tracer
invocation in the README. A fetched mark has a URL to check; a traced one has
nothing to reproduce it from unless the parameters are written down. Driven red
by replacing the word `potrace` in the README.

The mask-set expectations do need extending: `hermes` joins the pinned list of
marks that must be masked, while `gajae` and `mcode` are caught by the existing
multi-color assertion the moment they are added to the set by mistake.

## Verification

`cd gui && bun test tests/client-marks-assets.test.ts` plus the mask guard.
A 20px render of each new mark on `#ffffff` and on `#0d1117`, which is the check
that caught the Hermes dark-mode invisibility in the first place.
Loading
Loading