@flame0510/project-aether 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/agents/BrowserAccessSection.tsx +3 -3
- package/app/api/agents/[id]/open-control-ui/route.ts +1 -1
- package/app/api/agents/devices-summary/route.ts +2 -3
- package/app/api/gateway/sync.ts +5 -4
- package/app/api/update-check/route.ts +26 -15
- package/app/components/VersionBanner.tsx +77 -12
- package/docs/ARCHITECTURE.md +16 -12
- package/docs/REV4A.md +3 -4
- package/docs/dev/API-REFERENCE.md +36 -22
- package/docs/dev/GATEWAY.md +9 -9
- package/docs/rag/REV4A-OVERVIEW.md +1 -1
- package/lib/agent-devices.ts +12 -25
- package/lib/agent-images.ts +1 -1
- package/lib/agent-readiness.ts +11 -20
- package/lib/agent-update.ts +35 -11
- package/lib/agent-versions.json +0 -4
- package/lib/agent-versions.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
6
|
-
*
|
|
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';
|
package/app/api/gateway/sync.ts
CHANGED
|
@@ -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):
|
|
42
|
-
*
|
|
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
|
|
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);
|
|
@@ -68,45 +68,56 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
return NextResponse.json({ installed, latest, updateAvailable, package: getPackageInfo().name });
|
|
71
|
+
return NextResponse.json({ installed, latest, updateAvailable, package: getPackageInfo().name, log: 'update.log' });
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* POST /api/update-check
|
|
76
76
|
*
|
|
77
|
-
* Spawns rev4a update as a detached background process
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Spawns `rev4a update` as a detached background process: it installs the latest
|
|
78
|
+
* version, writes `.restart-flag` and lets `rev4a serve` restart the child (`rev4a
|
|
79
|
+
* _run`) with the new binary. Nothing here has to restart anything — the update kills
|
|
80
|
+
* this very process on its way in (`fuser -k <port>/tcp`), so any callback meant to run
|
|
81
|
+
* after it is dead code; the CLI's own flag is the only mechanism that has to work.
|
|
82
|
+
*
|
|
83
|
+
* The child's output goes to `update.log` in the data directory: since the update
|
|
84
|
+
* restarts this process mid-run, that file is the only record of what happened.
|
|
81
85
|
*/
|
|
82
86
|
export async function POST(request: Request): Promise<NextResponse> {
|
|
83
87
|
const denied = await requireAuthJWT(request);
|
|
84
88
|
if (denied) return denied;
|
|
85
89
|
|
|
86
|
-
// Spawn rev4a update in background, then write restart flag directly
|
|
87
|
-
// from this process so rev4a serve detects it and restarts.
|
|
88
90
|
const { spawn } = require('child_process');
|
|
89
91
|
const { join } = require('path');
|
|
90
|
-
const {
|
|
92
|
+
const { openSync, closeSync, appendFileSync } = require('fs');
|
|
91
93
|
|
|
92
94
|
const dataDir = process.env.REV4A_DATA_DIR ||
|
|
93
95
|
join(process.env.HOME || '/root', '.config', 'rev4a', 'data');
|
|
96
|
+
const logPath = join(dataDir, 'update.log');
|
|
97
|
+
|
|
98
|
+
let log: number | null = null;
|
|
99
|
+
try {
|
|
100
|
+
log = openSync(logPath, 'a');
|
|
101
|
+
appendFileSync(logPath, `\n[${new Date().toISOString()}] rev4a update, started from the dashboard\n`);
|
|
102
|
+
} catch {
|
|
103
|
+
// Unwritable log: run the update anyway, we just lose the record.
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
const child = spawn('rev4a', ['update'], {
|
|
96
|
-
stdio: 'ignore',
|
|
107
|
+
stdio: log === null ? 'ignore' : ['ignore', log, log],
|
|
97
108
|
detached: true,
|
|
109
|
+
cwd: dataDir,
|
|
98
110
|
});
|
|
111
|
+
if (log !== null) closeSync(log);
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
child.on('exit', () => {
|
|
103
|
-
try { writeFileSync(join(dataDir, '.restart-flag'), ''); } catch {}
|
|
113
|
+
child.on('error', (e: Error) => {
|
|
114
|
+
try { appendFileSync(logPath, `[dashboard] could not start the update: ${e.message}\n`); } catch {}
|
|
104
115
|
});
|
|
105
|
-
|
|
106
116
|
child.unref();
|
|
107
117
|
|
|
108
118
|
return NextResponse.json({
|
|
109
119
|
success: true,
|
|
110
|
-
message: 'Update started.
|
|
120
|
+
message: 'Update started. Rev4a restarts itself once the new version is installed.',
|
|
121
|
+
log: 'update.log',
|
|
111
122
|
});
|
|
112
123
|
}
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* "Update available" banner for Rev4a itself.
|
|
5
|
+
*
|
|
6
|
+
* The update is spawned by `POST /api/update-check` and restarts the server, so this
|
|
7
|
+
* component cannot be told the outcome by the response. It records what it asked for,
|
|
8
|
+
* waits for the server to answer again, and on the next mount compares: if the
|
|
9
|
+
* installed version moved it was updated, and if the same update is still offered it
|
|
10
|
+
* did not complete (the log file says why).
|
|
11
|
+
*/
|
|
3
12
|
import { useEffect, useState } from 'react';
|
|
4
13
|
|
|
5
14
|
interface VersionInfo {
|
|
6
15
|
installed: string;
|
|
7
16
|
latest: string | null;
|
|
8
17
|
updateAvailable: boolean;
|
|
18
|
+
log?: string;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
const DISMISS_KEY = 'rev4a-update-dismissed';
|
|
22
|
+
/** What we asked for and from which version, so the next mount can judge the outcome. */
|
|
23
|
+
const PENDING_KEY = 'rev4a-update-pending';
|
|
24
|
+
|
|
25
|
+
/** How long to poll for the restart before reloading anyway. */
|
|
26
|
+
const WAIT_MS = 120_000;
|
|
27
|
+
|
|
28
|
+
interface Pending { target: string; from: string }
|
|
29
|
+
|
|
30
|
+
function readPending(): Pending | null {
|
|
31
|
+
try {
|
|
32
|
+
const raw = sessionStorage.getItem(PENDING_KEY);
|
|
33
|
+
if (!raw) return null;
|
|
34
|
+
const parsed = JSON.parse(raw) as Pending;
|
|
35
|
+
return parsed && typeof parsed.target === 'string' && typeof parsed.from === 'string' ? parsed : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
12
40
|
|
|
13
41
|
export default function VersionBanner() {
|
|
14
42
|
const [info, setInfo] = useState<VersionInfo | null>(null);
|
|
@@ -18,17 +46,37 @@ export default function VersionBanner() {
|
|
|
18
46
|
const [updateOk, setUpdateOk] = useState<string | null>(null);
|
|
19
47
|
|
|
20
48
|
useEffect(() => {
|
|
49
|
+
let active = true;
|
|
21
50
|
fetch('/api/update-check?check=1')
|
|
22
51
|
.then((r) => r.json())
|
|
23
52
|
.then((d: VersionInfo) => {
|
|
53
|
+
if (!active) return;
|
|
54
|
+
const pending = readPending();
|
|
55
|
+
// Back from an update: did the installed version actually move?
|
|
56
|
+
if (pending && d.installed !== pending.from) {
|
|
57
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
58
|
+
setUpdateOk(`Updated to v${d.installed}`);
|
|
59
|
+
setTimeout(() => { if (active) window.location.reload(); }, 2000);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (pending) {
|
|
63
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
64
|
+
setInfo(d);
|
|
65
|
+
setDismissed(false);
|
|
66
|
+
setUpdateError(
|
|
67
|
+
`The update to v${pending.target} did not complete: Rev4a is still on v${d.installed}.`
|
|
68
|
+
+ ` See ${d.log ?? 'update.log'} in the Rev4a data directory.`,
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
24
72
|
if (d.updateAvailable) {
|
|
25
73
|
setInfo(d);
|
|
26
74
|
const saved = localStorage.getItem(DISMISS_KEY);
|
|
27
|
-
|
|
28
|
-
else setDismissed(false);
|
|
75
|
+
setDismissed(saved === d.latest);
|
|
29
76
|
}
|
|
30
77
|
})
|
|
31
78
|
.catch(() => {});
|
|
79
|
+
return () => { active = false; };
|
|
32
80
|
}, []);
|
|
33
81
|
|
|
34
82
|
function handleDismiss() {
|
|
@@ -39,6 +87,7 @@ export default function VersionBanner() {
|
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
async function handleUpdate() {
|
|
90
|
+
if (!info) return;
|
|
42
91
|
setUpdating(true);
|
|
43
92
|
setUpdateError(null);
|
|
44
93
|
setUpdateOk(null);
|
|
@@ -49,21 +98,37 @@ export default function VersionBanner() {
|
|
|
49
98
|
headers: { 'Content-Type': 'application/json' },
|
|
50
99
|
});
|
|
51
100
|
const data = await res.json();
|
|
52
|
-
if (data.success) {
|
|
53
|
-
setUpdateOk('Update started. Restarting\u2026');
|
|
54
|
-
localStorage.removeItem(DISMISS_KEY);
|
|
55
|
-
setTimeout(function() { window.location.reload(); }, 5000);
|
|
56
|
-
} else {
|
|
101
|
+
if (!data.success) {
|
|
57
102
|
setUpdateError(data.error || 'Update failed');
|
|
58
103
|
setUpdating(false);
|
|
104
|
+
return;
|
|
59
105
|
}
|
|
106
|
+
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ target: info.latest ?? '', from: info.installed }));
|
|
107
|
+
localStorage.removeItem(DISMISS_KEY);
|
|
108
|
+
setUpdateOk('Update started. Rev4a is restarting…');
|
|
109
|
+
// The server goes down and comes back on the new version: wait for its answer,
|
|
110
|
+
// then reload. A timeout reloads too — the next mount reports the outcome.
|
|
111
|
+
const deadline = Date.now() + WAIT_MS;
|
|
112
|
+
const poll = async () => {
|
|
113
|
+
if (Date.now() > deadline) { window.location.reload(); return; }
|
|
114
|
+
try {
|
|
115
|
+
const r = await fetch('/api/update-check', { cache: 'no-store' });
|
|
116
|
+
const d = (await r.json()) as VersionInfo;
|
|
117
|
+
if (d.installed !== info.installed) { window.location.reload(); return; }
|
|
118
|
+
} catch { /* the server is down for the update: expected */ }
|
|
119
|
+
setTimeout(poll, 3000);
|
|
120
|
+
};
|
|
121
|
+
setTimeout(poll, 3000);
|
|
60
122
|
} catch (err) {
|
|
61
123
|
setUpdateError(err instanceof Error ? err.message : 'Network error');
|
|
62
124
|
setUpdating(false);
|
|
63
125
|
}
|
|
64
126
|
}
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
// The result states stand on their own (there may be no `info` when the update just
|
|
129
|
+
// succeeded); the "available" state needs the version info and no dismissal.
|
|
130
|
+
const show = Boolean(updateOk) || Boolean(updateError) || Boolean(info?.updateAvailable && !dismissed);
|
|
131
|
+
if (!show) return null;
|
|
67
132
|
|
|
68
133
|
return (
|
|
69
134
|
<div className="update-banner">
|
|
@@ -73,25 +138,25 @@ export default function VersionBanner() {
|
|
|
73
138
|
<span className="update-banner__ok">{updateOk}</span>
|
|
74
139
|
) : updateError ? (
|
|
75
140
|
<span className="update-banner__err">{updateError}</span>
|
|
76
|
-
) : (
|
|
141
|
+
) : info ? (
|
|
77
142
|
<>
|
|
78
143
|
<span className="update-banner__label">Update available</span>
|
|
79
144
|
<span className="update-banner__sep">—</span>
|
|
80
145
|
<span className="update-banner__version">v{info.latest}</span>
|
|
81
146
|
<span className="update-banner__sep">(installed: v{info.installed})</span>
|
|
82
147
|
</>
|
|
83
|
-
)}
|
|
148
|
+
) : null}
|
|
84
149
|
</span>
|
|
85
150
|
|
|
86
151
|
{!updateOk && (
|
|
87
152
|
<div className="update-banner__actions">
|
|
88
153
|
{updateError ? (
|
|
89
154
|
<button className="ui-btn ui-btn--primary ui-btn--sm" onClick={handleUpdate} disabled={updating}>
|
|
90
|
-
{updating ? 'Updating
|
|
155
|
+
{updating ? 'Updating…' : 'Retry'}
|
|
91
156
|
</button>
|
|
92
157
|
) : (
|
|
93
158
|
<button className="ui-btn ui-btn--primary ui-btn--sm" onClick={handleUpdate} disabled={updating}>
|
|
94
|
-
{updating ? 'Updating
|
|
159
|
+
{updating ? 'Updating…' : 'Update now'}
|
|
95
160
|
</button>
|
|
96
161
|
)}
|
|
97
162
|
<button className="ui-btn ui-btn--ghost ui-btn--sm" onClick={handleDismiss}>
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
138
|
-
reject — one unknown value discards the whole generated
|
|
139
|
-
|
|
140
|
-
pull
|
|
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.
|
|
213
|
-
|
|
214
|
-
|
|
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 (
|
|
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
|
|
330
|
-
|
|
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.
|
|
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.
|
|
1174
|
-
"candidate": { "fromVersion": "2026.
|
|
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.
|
|
1178
|
-
"backupFile": "agent-agent_9253eee3-preupdate-2026.
|
|
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 (
|
|
1201
|
-
|
|
1202
|
-
|
|
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.
|
|
1388
|
-
|
|
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`.
|
|
1416
|
-
and
|
|
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
|
-
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
-
|
|
1523
|
-
|
|
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
|
|
@@ -2559,14 +2559,28 @@ Installed version against the latest published one.
|
|
|
2559
2559
|
**Query params:** `check=1` queries the registry; without it the answer comes
|
|
2560
2560
|
from local state only.
|
|
2561
2561
|
|
|
2562
|
-
**Response:** `{ "installed": "1.
|
|
2562
|
+
**Response:** `{ "installed": "1.4.2", "latest": "1.5.0", "updateAvailable": true, "package": "@flame0510/project-aether", "log": "update.log" }`
|
|
2563
|
+
|
|
2564
|
+
`log` is the file name, in the Rev4a data directory, where the update's output is
|
|
2565
|
+
written.
|
|
2563
2566
|
|
|
2564
2567
|
### `POST /api/update-check`
|
|
2565
|
-
Run `rev4a update` as a detached background process.
|
|
2566
|
-
|
|
2568
|
+
Run `rev4a update` as a detached background process. Nothing here restarts anything:
|
|
2569
|
+
the update kills this process on its way in (`fuser -k <port>/tcp`) and `rev4a serve`
|
|
2570
|
+
brings the child back on the new binary via its `.restart-flag`, so the connection
|
|
2571
|
+
drops and the page has to wait for the server to answer again.
|
|
2572
|
+
|
|
2573
|
+
The child's output goes to `update.log` in the data directory — the update restarts
|
|
2574
|
+
the process that spawned it, so that file is the only record of what happened.
|
|
2567
2575
|
|
|
2568
2576
|
**Auth:** browser cookie or bearer token
|
|
2569
2577
|
|
|
2578
|
+
**Response:** `{ "success": true, "message": "...", "log": "update.log" }`
|
|
2579
|
+
|
|
2580
|
+
The banner records what it asked for, polls `/api/version` until the installed version
|
|
2581
|
+
moves (or two minutes pass), reloads, and on the next mount either confirms
|
|
2582
|
+
"Updated to vX" or reports that the update did not complete and points at the log.
|
|
2583
|
+
|
|
2570
2584
|
### `GET /api/alerts/smoke`
|
|
2571
2585
|
Send a test alert, to verify the Telegram alerting path end to end.
|
|
2572
2586
|
|
package/docs/dev/GATEWAY.md
CHANGED
|
@@ -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.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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()`):
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
(`model catalog load issue`). That is why
|
|
373
|
-
|
|
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 (
|
|
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
|
package/lib/agent-devices.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
128
|
-
*
|
|
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
|
-
|
|
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
|
|
333
|
-
*
|
|
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,
|
package/lib/agent-images.ts
CHANGED
|
@@ -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 (
|
|
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(
|
package/lib/agent-readiness.ts
CHANGED
|
@@ -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
|
-
* **
|
|
18
|
-
* draining, 200 with `{"status":"started"}` when done. The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
package/lib/agent-update.ts
CHANGED
|
@@ -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:
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/lib/agent-versions.json
CHANGED
package/lib/agent-versions.ts
CHANGED
|
@@ -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
|
|
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;
|