@flame0510/project-aether 1.4.2 → 1.5.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.
@@ -6,11 +6,11 @@
6
6
  * From OpenClaw 9.x every new browser must be approved once before the Control UI
7
7
  * connects. This section lists the requests waiting for approval and the browsers
8
8
  * already approved, with approve, reject, rename and revoke. Agents created on
9
- * 2026.7.x run with device approval disabled and have nothing to list.
9
+ * Every supported agent lists its browsers through the CLI.
10
10
  *
11
11
  * `OpenControlUiButton` opens the Control UI through a one-time link that pairs the
12
12
  * browser with no approval, or through the plain token link when no one-time link can
13
- * be issued (2026.7.x has no `openclaw dashboard --json`).
13
+ * be issued.
14
14
  *
15
15
  * Data comes from `/api/agents/[id]/devices`, which runs the OpenClaw CLI inside the
16
16
  * container. It refreshes every few seconds only while this section is mounted.
@@ -352,7 +352,7 @@ export default function BrowserAccessSection({
352
352
 
353
353
  {access?.requiresApproval === false && (
354
354
  <div style={metaStyle}>
355
- This agent runs with browser approval disabled (OpenClaw 2026.7.x): its Control UI opens with the token alone,
355
+ This agent could not be read: its Control UI may open with the token alone,
356
356
  so there is nothing to approve.
357
357
  </div>
358
358
  )}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * What the "Open" buttons navigate to, in a new tab. Redirects to the agent's Control
5
5
  * UI: through a one-time link that pairs the browser with no approval on OpenClaw 9.x,
6
- * or through the plain token link on 2026.7.x or when no one-time link can be issued.
6
+ * or through the plain token link when no one-time link can be issued.
7
7
  *
8
8
  * The target host is the one this request reached Rev4a on (the `Host` header), with
9
9
  * the agent's published port. It is never a parameter: a page elsewhere that gets a
@@ -2,9 +2,8 @@
2
2
  * GET /api/agents/devices-summary
3
3
  *
4
4
  * Browsers waiting for approval, per running agent — the "browser waiting" badge on
5
- * the agent cards. Agents whose config disables device auth (2026.7.x) are answered
6
- * from the config alone, without the CLI. One agent that cannot be read reports
7
- * `error: true` and does not blank the others.
5
+ * the agent cards. One agent that cannot be read reports `error: true` and does not
6
+ * blank the others.
8
7
  */
9
8
  import { NextResponse, type NextRequest } from 'next/server';
10
9
  import { requireAuthJWT } from '@/lib/rev4a-auth';
@@ -38,8 +38,9 @@ const PROVIDER_GATEWAY_BASE_URL = detectProviderGatewayUrl();
38
38
  */
39
39
  /**
40
40
  * Model `input` values an agent's OpenClaw version accepts. The list is per version
41
- * (lib/agent-versions.json): 7.1-2 knows only `text | image`, and one value it does not
42
- * know invalidates the whole generated catalogue ("model catalog load issue").
41
+ * (lib/agent-versions.json): a value the running Gateway does not know invalidates the
42
+ * whole generated catalogue ("model catalog load issue"), and the accepted set grows
43
+ * between releases.
43
44
  */
44
45
  function modalityToInput(modality: string, version?: string | null): string[] {
45
46
  const allowed = new Set(modelInputFor(version));
@@ -376,8 +377,8 @@ export function syncAllAgents(): SyncOutcome {
376
377
  continue;
377
378
  }
378
379
  try {
379
- // Per agent: the `input` list follows the version that agent runs, so a 7.x
380
- // release never receives a modality value it would reject.
380
+ // Per agent: the `input` list follows the version that agent runs, so an agent on
381
+ // an older supported release never receives a modality value it would reject.
381
382
  syncContainer(container, buildRev4aProviderConfig(containerVersionSync(container.name)));
382
383
  outcome.synced.push(container.name);
383
384
  if (!container.running) outcome.stopped.push(container.name);
@@ -120,11 +120,13 @@ The central container, running the Next.js dashboard + orchestration API.
120
120
  - Immutable by agents — enforced by the `:ro` mount
121
121
  - See `lib/agent-setup.ts` for the centralized volume + config guarantee logic
122
122
  - **Waiting for a new container** is `waitForGatewayReady()` in
123
- `lib/agent-readiness.ts`, used by create, recreate and the agent route. On
124
- OpenClaw 9.x it waits for `/startupz` to report `started` (503 while starting).
125
- The 2026.7.1-2 image has no `/startupz` — its gateway serves the web UI with 200
126
- for unknown paths — so there it falls back to `/health`, which only shows the
127
- server is listening. Do not reimplement the wait in a route.
123
+ `lib/agent-readiness.ts`, used by create, recreate and the agent route. It waits for
124
+ `/startupz` to report `started` (503 while starting or draining, a non-JSON 200 or no
125
+ answer mean "not yet"). A Gateway that refused readiness — one that found a legacy
126
+ session store and wants `doctor --fix` — answers 200 on `/health` while draining; the
127
+ old fallback that accepted that as "ready" was removed with 7.x support, because it
128
+ let callers configure an agent that was still migrating. Do not reimplement the wait in
129
+ a route.
128
130
  - **The model catalogue** is read only through `lib/model-catalogue.ts`:
129
131
  `loadModelsConfig()`, `loadOfferedModels()` (enabled after overrides, provider has
130
132
  a key), `isModelOffered()` (enforced by the proxy and the assistant) and
@@ -134,10 +136,12 @@ The central container, running the Next.js dashboard + orchestration API.
134
136
  OpenClaw versions, newest first, with the model `input` list for each; the server reads
135
137
  it through `lib/agent-versions.ts`, the `rev4a` CLI with `require`. The provider sync
136
138
  takes each agent's own list (`containerVersionSync()`: image tag, else the entrypoint's
137
- `.last-version` file), so a 2026.7.x agent is never sent an `input` value it would
138
- reject — one unknown value discards the whole generated catalogue. The previous
139
- versions stay listed while agents still run them: that is also what lets a rollback
140
- pull the previous image. `lib/agent-images.ts`
139
+ `.last-version` file), so an agent on an older supported release is never sent an
140
+ `input` value it would reject — one unknown value discards the whole generated
141
+ catalogue. A version stays listed while agents still run it, and listing it is also what
142
+ lets a rollback pull its image: since 2026.9.3 is the only entry, a rollback whose target
143
+ is no longer listed is refused up front (`startAgentRollback()`) instead of restoring a
144
+ volume on a version this build cannot run. `lib/agent-images.ts`
141
145
  lists local `openclaw-agent-base:<version>` images through the Docker API, reads an
142
146
  agent's version (image label or tag, else `openclaw --version` in the container, cached
143
147
  per image id), reads the registry tag list over HTTP (cached 10 min), pulls a version
@@ -209,9 +213,9 @@ The central container, running the Next.js dashboard + orchestration API.
209
213
  `openclaw devices list | approve | reject | rename | remove` and
210
214
  `openclaw dashboard --json`, run inside the container with the async `dockerExec`.
211
215
  The gateway token is resolved inside the container (`/root/.agent-token`, then its
212
- environment), never passed in argv. Whether approval applies is decided by the
213
- OpenClaw version: 9.x answers `/startupz` with JSON and always requires it;
214
- 2026.7.x follows `gateway.controlUi.dangerouslyDisableDeviceAuth`.
216
+ environment), never passed in argv. Every supported OpenClaw answers `/startupz` with
217
+ JSON and always requires approval; when that read fails the panel says it could not
218
+ tell, instead of guessing.
215
219
  `buildInviteLink()` builds the plain token link for someone else; the invite route
216
220
  refuses agents without approval. Every link's token is read inside the container,
217
221
  and its host is the request's `Host` header (`hostnameFromHostHeader()`), never a
package/docs/REV4A.md CHANGED
@@ -306,7 +306,7 @@ Lists all agents (Docker containers with `AGENT_ID` label), with:
306
306
  - **Control UI link** — the **Open** buttons go through `GET /api/agents/[id]/open-control-ui`, which redirects to the agent's published port on the host the dashboard was reached on: a one-time link on OpenClaw 9.x, the plain `#token=` link otherwise, with the token read inside the container.
307
307
  - **Agent creation wizard** — multi-step form at `/agents/create`.
308
308
  - **Channel Manager** — each agent detail panel has a Channels section with Telegram configuration and pairing management. Pending requests (a sender's `/start` creates one) show an **Approve** button; approved senders show **Revoke** with a confirmation. Approved senders are OpenClaw's pairing store, not the agent config: on 2026.9.x the SQLite store, read and revoked through OpenClaw's own store functions (see [ARCHITECTURE.md](ARCHITECTURE.md)).
309
- - **Browser access** — the detail panel section right after Channels. From OpenClaw 9.x each new browser must be approved once before the Control UI connects: the section lists requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), and the agent card shows a "browser waiting" badge. The **Open** buttons first ask the agent for a one-time link that pairs the browser with no approval, and open the plain token link when none can be issued (every 2026.7.x agent). **Invite link** (9.x agents only) gives a link to send to someone else: their browser passes gateway auth and waits in the approval list until approved. The link carries the gateway token every agent shares; changing the agents token invalidates every link sent. All of it runs the OpenClaw CLI inside the container asynchronously.
309
+ - **Browser access** — the detail panel section right after Channels. From OpenClaw 9.x each new browser must be approved once before the Control UI connects: the section lists requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), and the agent card shows a "browser waiting" badge. The **Open** buttons first ask the agent for a one-time link that pairs the browser with no approval, and open the plain token link when none can be issued (the agent is stopped, or the link fails). **Invite link** gives a link to send to someone else: their browser passes gateway auth and waits in the approval list until approved. The link carries the gateway token every agent shares; changing the agents token invalidates every link sent. All of it runs the OpenClaw CLI inside the container asynchronously.
310
310
 
311
311
  ### Agent Creation Wizard (`/agents/create`)
312
312
 
@@ -326,9 +326,8 @@ Available templates:
326
326
  1. The container boots with the newest supported OpenClaw version downloaded here, `openclaw-agent-base:<version>`; the create answers `409` when none is — see [Agent Templates](#agent-templates) below.
327
327
  2. OpenClaw gateway starts automatically with `--allow-unconfigured`, generating its own default config.
328
328
  3. The route waits for the gateway to finish starting with `waitForGatewayReady()`, up
329
- to 60 s. On OpenClaw 9.x it waits for `/startupz` to report `started`. The
330
- 2026.7.1-2 image has no `/startupz`, so there it waits for `/health`, which only
331
- shows the server is listening.
329
+ to 60 s: `/startupz` has to report `started`. A 503 (starting or draining), a
330
+ non-JSON 200 or no answer all mean "not yet".
332
331
  4. Once ready, the route writes:
333
332
  - `gateway.controlUi` with the browser-origin policy (`withControlUiPolicy()` in `lib/agent-setup.ts`): the Host-header fallback on, so the Control UI is accepted from the host the browser connected to — a public IP is refused without it — while pages from other origins are refused; an `allowedOrigins: ["*"]` list and the retired `dangerouslyDisableDeviceAuth` are dropped, any other allowlist is kept. Recreate re-applies the policy. Each new browser still needs a one-time device approval
334
333
  - `agents.defaults.model.primary` + fallbacks — the primary model selected in the wizard
@@ -1148,7 +1148,7 @@ agent was never recreated).
1148
1148
  {
1149
1149
  "recreate": {
1150
1150
  "id": 12, "agentId": "agent_9253eee3", "status": "backing_up",
1151
- "image": "openclaw-agent-base:2026.7.1-2",
1151
+ "image": "openclaw-agent-base:2026.9.3",
1152
1152
  "backupFile": "agent-agent_9253eee3-prerecreate-2026-09-17_171000.tar.gz",
1153
1153
  "backupPercent": 40, "error": null,
1154
1154
  "startedAtMs": 1789656372120, "finishedAtMs": null
@@ -1170,12 +1170,12 @@ The agent's OpenClaw version, the update it could take, and its latest update
1170
1170
 
1171
1171
  ```json
1172
1172
  {
1173
- "currentVersion": "2026.7.1-2",
1174
- "candidate": { "fromVersion": "2026.7.1-2", "toVersion": "2026.9.3" },
1173
+ "currentVersion": "2026.9.3",
1174
+ "candidate": { "fromVersion": "2026.9.3", "toVersion": "2026.9.4" },
1175
1175
  "update": {
1176
1176
  "id": 3, "agentId": "agent_9253eee3", "status": "backing_up",
1177
- "fromVersion": "2026.7.1-2", "toVersion": "2026.9.3",
1178
- "backupFile": "agent-agent_9253eee3-preupdate-2026.7.1-2-2026-09-16_003512345.tar.gz",
1177
+ "fromVersion": "2026.9.3", "toVersion": "2026.9.4",
1178
+ "backupFile": "agent-agent_9253eee3-preupdate-2026.9.3-2026-09-16_003512345.tar.gz",
1179
1179
  "backupPercent": 40, "verification": null, "error": null,
1180
1180
  "startedAtMs": 1789524912000, "finishedAtMs": null
1181
1181
  }
@@ -1197,9 +1197,10 @@ the background.
1197
1197
 
1198
1198
  1. **Preflight** (refused with `409`): the agent runs, the target is downloaded,
1199
1199
  supported and newer than the agent's version, no update or backup is running.
1200
- 2. **Baseline**, inside the agent: transcript events per session (`.jsonl` lines on
1201
- 2026.7.x, `transcript_events` rows from 9.x; trajectory files not counted) and cron
1202
- job names.
1200
+ 2. **Baseline**, inside the agent: transcript events per session (`transcript_events`
1201
+ rows in `openclaw-agent.sqlite`, plus `sessions/*.jsonl` and the
1202
+ `session-sqlite-import-archive/` the 9.x import moves old files into, taking the
1203
+ maximum per session; trajectory files not counted) and cron job names.
1203
1204
  3. **`backing_up`**: cold backup `agent-<id>-preupdate-<from>-<ts>.tar.gz`
1204
1205
  (`startColdBackup`, agent left stopped).
1205
1206
  4. **`migrating`**: the container is recreated on `openclaw-agent-base:<to>`
@@ -1384,8 +1385,8 @@ There is no CLI or RPC for this (`openclaw pairing` covers pending requests only
1384
1385
  `channels.pairing.*` has no remove — verified on 2026.9.3). The pairing store's own
1385
1386
  writer is used instead: `removeChannelAllowFromStoreEntry`, located by function name in
1386
1387
  OpenClaw's `pairing-store` module and run inside the container, so the write goes
1387
- through the same state transaction the CLI uses. Verifying a 2026.7.x agent is out of
1388
- scope: Rev4a targets 2026.9.3. On a release that no longer exposes the writer the call
1388
+ through the same state transaction the CLI uses. Rev4a supports 2026.9.x only, so no
1389
+ older layout is handled. On a release that no longer exposes the writer the call
1389
1390
  fails with a message pointing at `/allowlist remove` from the chat — nothing is silently
1390
1391
  left unchanged.
1391
1392
 
@@ -1412,9 +1413,8 @@ already approved.
1412
1413
  **Auth:** browser cookie
1413
1414
 
1414
1415
  Runs `openclaw devices list --json` inside the container with the async `dockerExec`;
1415
- see `lib/agent-devices.ts`. On OpenClaw 2026.7.x, which answers `/startupz` with HTML
1416
- and whose config disables device approval, the CLI is not called and both lists are
1417
- empty.
1416
+ see `lib/agent-devices.ts`. When the Gateway cannot be read `requiresApproval` is null
1417
+ and both lists are empty.
1418
1418
 
1419
1419
  **Response:**
1420
1420
  ```json
@@ -1515,12 +1515,12 @@ Control UI.
1515
1515
  The target host is the one the request reached Rev4a on (the `Host` header), with the
1516
1516
  agent's published port — never a parameter, so the link cannot be sent to another host.
1517
1517
 
1518
- - **OpenClaw 9.x** (it answers `/startupz` with JSON): runs `openclaw dashboard --json`
1519
- in the container and redirects to its `browserUrl`, with the host, the port and the
1520
- `gatewayUrl` fragment parameter rewritten to that host and the published port. The
1521
- link is single-use, expires after ten minutes, and pairs the browser with no approval.
1522
- - **OpenClaw 2026.7.x**, or when no one-time link can be issued: redirects to the plain
1523
- link `http://<host>:<port>/#token=<token>`, with the token read inside the container
1518
+ - Runs `openclaw dashboard --json` in the container and redirects to its `browserUrl`,
1519
+ with the host, the port and the `gatewayUrl` fragment parameter rewritten to that host
1520
+ and the published port. The link is single-use, expires after ten minutes, and pairs
1521
+ the browser with no approval.
1522
+ - When no one-time link can be issued: redirects to the plain link
1523
+ `http://<host>:<port>/#token=<token>`, with the token read inside the container
1524
1524
  (`/root/.agent-token`, then `OPENCLAW_GATEWAY_TOKEN`).
1525
1525
 
1526
1526
  A navigation, not a JSON call: the button opens this URL inside the click, so no tab
@@ -170,10 +170,10 @@ Saving calls `PUT /api/gateway/agent`, which writes the container's
170
170
  container's existing list untouched, `[]` clears it. The Model panel is the only
171
171
  UI that sets them; the create wizard does not
172
172
  - **no restart** — `agents` is in OpenClaw's hot-reload column, so the write
173
- applies on the agent's next turn. Exception on OpenClaw 2026.7.1-2: a Telegram
174
- channel keeps the configuration it had when it started, so Telegram replies stay on
175
- the old model until the container is restarted. The Control UI picks the change up
176
- at once. OpenClaw 9.x reads the configuration per message
173
+ applies on the agent's next turn. Current OpenClaw reads the configuration per
174
+ message; the Control UI picks the change up at once. (On 2026.7.1-2 a Telegram
175
+ channel kept the configuration it had when it started, so Telegram replies stayed on
176
+ the old model until the container was restarted — that build is no longer supported.)
177
177
 
178
178
  The agent list shows each agent's current model as a chip, fed by
179
179
  `GET /api/agents/models-summary`. The red states are split by remedy, because they
@@ -366,11 +366,11 @@ Restart the Rev4a server (via systemd).
366
366
  to the file directly without restarting the gateway.
367
367
 
368
368
  The model `input` list follows each agent's OpenClaw version
369
- (`lib/agent-versions.json`, resolved by `containerVersionSync()`): the newest release
370
- accepts `text | image | audio | video`, while `2026.7.1-2` accepts only `text | image`,
371
- and one unknown value makes it discard the whole generated catalogue
372
- (`model catalog load issue`). That is why the previous version stays in the table
373
- while agents still run it.
369
+ (`lib/agent-versions.json`, resolved by `containerVersionSync()`): 2026.9.3 accepts
370
+ `text | image | audio | video`, an older release may accept less, and one unknown
371
+ value makes it discard the whole generated catalogue
372
+ (`model catalog load issue`). That is why a version stays in the table while agents
373
+ still run it.
374
374
 
375
375
  5. **Entrypoint does not generate openclaw.json** — The entrypoint resolves the
376
376
  auth token and starts the gateway with `--allow-unconfigured`; OpenClaw
@@ -34,7 +34,7 @@ Manage Docker containers running OpenClaw agents. See which agents are running,
34
34
 
35
35
  The agent list also shows a compact TG chip next to each agent name (green = connected, hidden if not configured).
36
36
 
37
- **Browser access:** on agents running OpenClaw 9.x, every new browser has to be approved once before the agent's Control UI connects. The detail panel's "BROWSER ACCESS" section lists browsers waiting for approval, with Approve and Reject, and the browsers already approved, with Rename and Revoke. A yellow "BROWSER WAITING" chip on the agent card says a request is pending. The **Open** button tries a one-time link that lets the browser in without any approval; if the agent cannot issue one (agents still on OpenClaw 2026.7.x) it opens the normal link, which on those agents needs no approval anyway. To let someone else in, **Invite link** gives a link to send them: their browser appears under waiting for approval, and nothing opens until you approve it. The link contains the token all agents share; changing the agents token cancels every link already sent.
37
+ **Browser access:** on agents running OpenClaw 9.x, every new browser has to be approved once before the agent's Control UI connects. The detail panel's "BROWSER ACCESS" section lists browsers waiting for approval, with Approve and Reject, and the browsers already approved, with Rename and Revoke. A yellow "BROWSER WAITING" chip on the agent card says a request is pending. The **Open** button tries a one-time link that lets the browser in without any approval; if the agent cannot issue one (it is stopped, or the link fails) it opens the normal link. To let someone else in, **Invite link** gives a link to send them: their browser appears under waiting for approval, and nothing opens until you approve it. The link contains the token all agents share; changing the agents token cancels every link already sent.
38
38
 
39
39
  The Agents page includes a banner for the **agent base image**, which is kept per
40
40
  OpenClaw version. It is hidden when a supported version is downloaded and nothing newer
@@ -9,11 +9,8 @@
9
9
  * with the entrypoint's precedence — `/root/.agent-token`, then the container's
10
10
  * `OPENCLAW_GATEWAY_TOKEN` — so it never reaches argv on the host.
11
11
  *
12
- * Browser approval exists from OpenClaw 9.x. Agents created on 2026.7.x carry
13
- * `gateway.controlUi.dangerouslyDisableDeviceAuth: true`: they open with the token
14
- * alone and have nothing to approve, so `requiresApproval` is false for them and
15
- * the CLI is not called. `openclaw dashboard --json` does not exist on 2026.7.x
16
- * either, so `resolveControlUiUrl` gives those agents the plain token link.
12
+ * Browser approval is part of OpenClaw 9.x, the only major this build supports. An
13
+ * agent whose Gateway cannot be read answers "unknown" rather than guessing.
17
14
  */
18
15
  import { dockerExec, dockerExecNoFail } from '@/lib/docker-exec';
19
16
  import { dockerFetch } from '@/lib/docker-socket';
@@ -110,35 +107,26 @@ async function runCli(container: string, args: string[], timeoutMs = CLI_TIMEOUT
110
107
  return parseCliJson(out);
111
108
  }
112
109
 
113
- /** True when the Gateway answers `/startupz` with JSON carrying its version, which only OpenClaw 9.x does. */
110
+ /** True when the Gateway answers `/startupz` with JSON carrying its version: a 9.x Gateway. */
114
111
  async function answersStartupzWithVersion(container: string): Promise<boolean> {
115
112
  const probe = await dockerExecNoFail(container, ['curl', '-s', '--max-time', '3', 'http://127.0.0.1:3000/startupz'], { timeoutMs: CONFIG_TIMEOUT_MS });
116
113
  try {
117
114
  return typeof (JSON.parse(probe) as { version?: unknown })?.version === 'string';
118
115
  } catch {
119
- // Not JSON: the 2026.7.x web UI answers every unknown path with HTML.
116
+ // Not JSON (a 9.x Gateway that is still starting answers 503, and a stopped agent
117
+ // answers nothing): the version cannot be read.
120
118
  return false;
121
119
  }
122
120
  }
123
121
 
124
122
  /**
125
- * Whether browsers need approval on this agent.
126
- *
127
- * The OpenClaw version decides first. 9.x answers `/startupz` with a JSON document
128
- * carrying its version, and there approval always applies: it ignores
129
- * `dangerouslyDisableDeviceAuth`, which an agent created on 2026.7.x may still carry.
130
- * Anything else is 2026.7.x, where that key does disable approval. Null when neither
131
- * can be read.
123
+ * Whether browsers need approval on this agent. Every supported OpenClaw answers
124
+ * `/startupz` with a JSON document carrying its version, and there approval always
125
+ * applies. Null when the Gateway cannot be read, so callers can answer "unknown"
126
+ * instead of guessing.
132
127
  */
133
128
  export async function requiresBrowserApproval(container: string): Promise<boolean | null> {
134
- if (await answersStartupzWithVersion(container)) return true;
135
- try {
136
- const raw = await dockerExec(container, ['cat', '/root/.openclaw/openclaw.json'], { timeoutMs: CONFIG_TIMEOUT_MS });
137
- const cfg = JSON.parse(raw) as { gateway?: { controlUi?: { dangerouslyDisableDeviceAuth?: unknown } } };
138
- return cfg?.gateway?.controlUi?.dangerouslyDisableDeviceAuth !== true;
139
- } catch {
140
- return null;
141
- }
129
+ return (await answersStartupzWithVersion(container)) ? true : null;
142
130
  }
143
131
 
144
132
  const str = (v: unknown): string | null => (typeof v === 'string' && v.trim() ? v.trim() : null);
@@ -329,9 +317,8 @@ const plainLink = (hostname: string, port: string, token: string) =>
329
317
  `http://${hostForUrl(hostname)}:${port}/#token=${encodeURIComponent(token)}`;
330
318
 
331
319
  /**
332
- * Where "Open" should take the browser: the one-time link on OpenClaw 9.x, which pairs
333
- * the browser with no approval, else the plain token link — on 2026.7.x, which has no
334
- * `dashboard --json` and needs no approval, or when no one-time link could be issued.
320
+ * Where "Open" should take the browser: the one-time link, which pairs the browser with
321
+ * no approval, else the plain token link when no one-time link could be issued.
335
322
  */
336
323
  export async function resolveControlUiUrl(
337
324
  container: string,
@@ -128,7 +128,7 @@ const UNKNOWN_VERSION_TTL_MS = 5 * 60 * 1000;
128
128
  /**
129
129
  * The OpenClaw version a container runs: its image's label or version tag, else
130
130
  * `openclaw --version` inside the running container, for images built before the
131
- * label existed (2026.7.1-2 published as `:latest` only). Null for a stopped container
131
+ * label existed (a release published as `:latest` only). Null for a stopped container
132
132
  * whose image carries neither.
133
133
  */
134
134
  export async function containerOpenClawVersion(
@@ -14,16 +14,14 @@
14
14
  * route deletes the container when this probe times out, so a false negative
15
15
  * there destroys a healthy agent over an unrelated channel.
16
16
  *
17
- * **Two OpenClaw majors.** `/startupz` exists from 9.x: 503 while starting or
18
- * draining, 200 with `{"status":"started"}` when done. The 2026.7.1-2 image that
19
- * `openclaw-agent-base` currently pins has no `/startupz`; its Gateway answers
20
- * every unknown path with 200 and the web UI's HTML. So the probe reads the
21
- * status code *and* the body, and falls back to `/health` only for a 200 that is
22
- * not JSON — the old image. A 503, or no answer, means "not yet", never "use
23
- * `/health`": reading an empty answer as "old image" makes a 9.x Gateway count as
24
- * ready the moment it starts listening.
25
- *
26
- * Delete the `/health` fallback once every image is on 9.x.
17
+ * **The endpoint used.** `/startupz` exists from 9.x: 503 while starting or
18
+ * draining, 200 with `{"status":"started"}` when done. The probe reads the status
19
+ * code *and* the body: a 503, a non-JSON 200 or no answer means "not yet". A
20
+ * Gateway that refused readiness — for instance one that found a legacy session
21
+ * store and wants `doctor --fix` — answers 200 on `/health` while draining; reading
22
+ * that as ready would let the create route configure an agent that is still
23
+ * migrating (the 2026.7.1-2 fallback that used to do exactly that was removed with
24
+ * 7.x support).
27
25
  *
28
26
  * **Async.** A request path must not hold Node's single thread for up to a
29
27
  * minute; see docs/ARCHITECTURE.md §3.1.
@@ -88,16 +86,9 @@ export async function waitForGatewayReady(
88
86
 
89
87
  if (startupz.status === 200) {
90
88
  const json = asJson(startupz.body);
91
- if (json) {
92
- // 9.x: accept only the answer that means startup finished.
93
- if (json.status === 'started') return true;
94
- } else {
95
- // 200 with a non-JSON body: the 2026.7.1-2 web UI catch-all, so this build
96
- // has no /startupz. Liveness is the best signal it offers.
97
- const health = await probe(container, '/health');
98
- const h = health.status === 200 ? asJson(health.body) : null;
99
- if (h && (h.ok === true || h.status === 'live')) return true;
100
- }
89
+ // 9.x: accept only the answer that means startup finished. A 200 with any other
90
+ // body is not a ready Gateway.
91
+ if (json && json.status === 'started') return true;
101
92
  }
102
93
  // 503 (starting or draining), any other code, or no answer: not yet.
103
94
 
@@ -47,34 +47,51 @@ const COLD_BACKUP_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
47
47
  export class UpdateRefusedError extends Error {}
48
48
 
49
49
  /**
50
- * Counts transcript events per session, inside the agent: `.jsonl` lines on 2026.7.x,
51
- * `transcript_events` rows in `openclaw-agent.sqlite` from 9.x. Keys are
52
- * `<agent>/<session id>`; trajectory files are debug traces and are not counted.
50
+ * Counts transcript events per session, inside the agent: `transcript_events` rows in
51
+ * `openclaw-agent.sqlite`. Keys are `<agent>/<session id>`; trajectory files are debug
52
+ * traces and are not counted.
53
+ *
54
+ * Two other places are read as well, each taking the maximum per session (never the sum):
55
+ * - `sessions/*.jsonl`, for a build that still writes transcripts as files;
56
+ * - `session-sqlite-import-archive/archive-tier.<id>.jsonl.imported-<ts>`, where 9.x
57
+ * moves the old `.jsonl` files after importing them into SQLite. A session whose
58
+ * events were **not** imported lives only there, so without it the count drops to
59
+ * zero and a healthy migration reads as lost data (seen on the VPS with 10 short
60
+ * sessions of an agent: the archived file still held the same 8 lines).
53
61
  */
54
62
  const COUNT_TRANSCRIPTS = `
55
63
  const fs = require('fs'), path = require('path');
56
64
  const out = {};
65
+ const bump = (key, n) => { if (n > (out[key] || 0)) out[key] = n; };
66
+ const linesOf = (file) => {
67
+ try { return fs.readFileSync(file, 'utf8').split('\\n').filter((l) => l.trim()).length; } catch { return 0; }
68
+ };
57
69
  const root = '/root/.openclaw/agents';
58
70
  for (const agent of fs.existsSync(root) ? fs.readdirSync(root) : []) {
59
- const sessions = path.join(root, agent, 'sessions');
71
+ const base = path.join(root, agent);
72
+ const sessions = path.join(base, 'sessions');
60
73
  if (fs.existsSync(sessions)) {
61
74
  for (const f of fs.readdirSync(sessions)) {
62
75
  if (!f.endsWith('.jsonl') || f.endsWith('.trajectory.jsonl')) continue;
63
- const n = fs.readFileSync(path.join(sessions, f), 'utf8').split('\\n').filter((l) => l.trim()).length;
64
- const key = agent + '/' + f.slice(0, -'.jsonl'.length);
65
- out[key] = Math.max(out[key] || 0, n);
76
+ bump(agent + '/' + f.slice(0, -'.jsonl'.length), linesOf(path.join(sessions, f)));
77
+ }
78
+ }
79
+ const archive = path.join(base, 'session-sqlite-import-archive');
80
+ if (fs.existsSync(archive)) {
81
+ for (const f of fs.readdirSync(archive)) {
82
+ if (f.includes('.trajectory.jsonl')) continue;
83
+ const m = f.match(/^archive-tier\\.(.+?)\\.jsonl\\.imported-\\d+$/);
84
+ if (m) bump(agent + '/' + m[1], linesOf(path.join(archive, f)));
66
85
  }
67
86
  }
68
- const dbFile = path.join(root, agent, 'agent', 'openclaw-agent.sqlite');
87
+ const dbFile = path.join(base, 'agent', 'openclaw-agent.sqlite');
69
88
  if (fs.existsSync(dbFile)) {
70
89
  const { DatabaseSync } = require('node:sqlite');
71
90
  const db = new DatabaseSync(dbFile, { readOnly: true });
72
- // 2026.7.x creates this database too, without the transcript table.
73
91
  const hasEvents = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'transcript_events'").get();
74
92
  if (hasEvents) {
75
93
  for (const r of db.prepare('SELECT session_id, COUNT(*) AS n FROM transcript_events GROUP BY session_id').all()) {
76
- const key = agent + '/' + r.session_id;
77
- out[key] = Math.max(out[key] || 0, Number(r.n));
94
+ bump(agent + '/' + r.session_id, Number(r.n));
78
95
  }
79
96
  }
80
97
  db.close();
@@ -361,6 +378,13 @@ async function runRollbackStart(agentId: string): Promise<number> {
361
378
  const row = latestUpdate(agentId);
362
379
  if (!row || !ROLLBACK_FROM.includes(row.status)) throw new UpdateRefusedError('There is no update of this agent to roll back');
363
380
  if (!row.backup_file || !row.from_version) throw new UpdateRefusedError('This update has no pre-update backup to restore');
381
+ // Refused up front, not mid-way: this Rev4a supports 9.x only, so a row recorded
382
+ // before 2026.7.x support was dropped has no image this build can recreate, and the
383
+ // readiness probe would never see the old Gateway as ready either. Better to say so
384
+ // than to restore a volume and leave the agent on a version this build cannot run.
385
+ if (!isSupportedVersion(row.from_version)) {
386
+ throw new UpdateRefusedError(`This Rev4a no longer supports OpenClaw ${row.from_version} (the version this update moved away from), so it cannot roll back to it`);
387
+ }
364
388
  if (!(await backupFileExists(row.backup_file))) {
365
389
  throw new UpdateRefusedError(`The pre-update backup ${row.backup_file} no longer exists`);
366
390
  }
@@ -5,10 +5,6 @@
5
5
  {
6
6
  "version": "2026.9.3",
7
7
  "modelInput": ["text", "image", "audio", "video"]
8
- },
9
- {
10
- "version": "2026.7.1-2",
11
- "modelInput": ["text", "image"]
12
8
  }
13
9
  ]
14
10
  }
@@ -58,7 +58,7 @@ export function modelInputFor(version?: string | null): string[] {
58
58
 
59
59
  const VERSION_RE = /(\d{4})\.(\d{1,2})\.(\d{1,2})(?:-(\d+))?/;
60
60
 
61
- /** The version in `2026.9.3`, `2026.7.1-2` or `OpenClaw 2026.9.3 (1391f7c)`; null when there is none. */
61
+ /** The version in `2026.9.3` or `OpenClaw 2026.9.3 (1391f7c)`; null when there is none. */
62
62
  export function parseOpenClawVersion(text: string | null | undefined): string | null {
63
63
  const m = VERSION_RE.exec(text ?? '');
64
64
  if (!m) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flame0510/project-aether",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
5
5
  "keywords": [
6
6
  "openclaw",