@flame0510/project-aether 1.4.1 → 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.
- package/app/agents/BrowserAccessSection.tsx +3 -3
- package/app/agents/PageClient.tsx +31 -6
- package/app/agents/create/PageClient.tsx +19 -5
- 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/agents/ports/route.ts +19 -0
- package/app/api/gateway/sync.ts +5 -4
- package/docs/ARCHITECTURE.md +20 -12
- package/docs/FRONTEND-ARCHITECTURE.md +1 -1
- package/docs/REV4A.md +3 -4
- package/docs/dev/API-REFERENCE.md +40 -19
- 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
|
)}
|
|
@@ -770,6 +770,14 @@ function currentPortRange(ports: string): string {
|
|
|
770
770
|
return min === max ? `${min}` : `${min}-${max}`;
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
/** The agent's own published host ports, from the `3700:3000, 3701:3001` string. */
|
|
774
|
+
function ownHostPorts(ports: string): number[] {
|
|
775
|
+
return (ports || '')
|
|
776
|
+
.split(',')
|
|
777
|
+
.map((p) => parseInt(p.trim().split(':')[0] ?? '', 10))
|
|
778
|
+
.filter((n) => Number.isInteger(n));
|
|
779
|
+
}
|
|
780
|
+
|
|
773
781
|
function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
774
782
|
agent: Agent;
|
|
775
783
|
onClose: () => void;
|
|
@@ -780,20 +788,37 @@ function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
|
780
788
|
}) {
|
|
781
789
|
const initialName = agent.displayName || agent.name;
|
|
782
790
|
const initialPorts = currentPortRange(agent.ports);
|
|
791
|
+
const ownPorts = useMemo(() => ownHostPorts(agent.ports), [agent.ports]);
|
|
783
792
|
const [displayName, setDisplayName] = useState(initialName);
|
|
784
793
|
const [portRange, setPortRange] = useState(initialPorts);
|
|
785
794
|
const [saving, setSaving] = useState(false);
|
|
786
795
|
const [error, setError] = useState('');
|
|
796
|
+
// The ports other containers hold, fetched when the panel opens: the same set the
|
|
797
|
+
// create form gets as a page prop, from the same lookup (`GET /api/agents/ports`).
|
|
798
|
+
const [usedPorts, setUsedPorts] = useState<Set<number> | null>(null);
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
const controller = new AbortController();
|
|
802
|
+
fetch('/api/agents/ports', { signal: controller.signal })
|
|
803
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
804
|
+
.then((data: { ports?: number[] } | null) => {
|
|
805
|
+
if (data && Array.isArray(data.ports)) setUsedPorts(new Set(data.ports));
|
|
806
|
+
})
|
|
807
|
+
.catch((e: unknown) => { if ((e as Error)?.name !== 'AbortError') setUsedPorts(null); });
|
|
808
|
+
return () => controller.abort();
|
|
809
|
+
}, []);
|
|
787
810
|
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
811
|
+
// The same rules and messages as agent creation (lib/agent-ports.ts). This agent's own
|
|
812
|
+
// block is excluded, so keeping or shifting it is not a conflict with itself; every
|
|
813
|
+
// other container counts, running or stopped.
|
|
791
814
|
const nameError = displayName.trim() ? '' : 'Display name must not be empty';
|
|
792
815
|
const portError = useMemo(() => {
|
|
793
816
|
if (!portRange.trim()) return '';
|
|
794
|
-
const
|
|
817
|
+
const others = new Set(usedPorts ?? []);
|
|
818
|
+
for (const p of ownPorts) others.delete(p);
|
|
819
|
+
const r = validatePortInput(portRange, others);
|
|
795
820
|
return r.valid ? '' : r.error;
|
|
796
|
-
}, [portRange]);
|
|
821
|
+
}, [portRange, usedPorts, ownPorts]);
|
|
797
822
|
|
|
798
823
|
const nameChanged = displayName.trim() !== initialName;
|
|
799
824
|
const portsChanged = portRange.trim() !== initialPorts;
|
|
@@ -882,7 +907,7 @@ function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
|
882
907
|
{error && <div style={{ fontSize: 11, color: '#ef4444' }}>{error}</div>}
|
|
883
908
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
884
909
|
<Button variant="secondary" size="sm" onClick={onClose} disabled={saving}>Cancel</Button>
|
|
885
|
-
<Button variant="primary" size="sm" onClick={handleSave} disabled={saving}>
|
|
910
|
+
<Button variant="primary" size="sm" onClick={handleSave} disabled={saving || !!nameError || !!portError}>
|
|
886
911
|
{saving ? 'Saving...' : 'Save & Recreate'}
|
|
887
912
|
</Button>
|
|
888
913
|
</div>
|
|
@@ -60,6 +60,20 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
60
60
|
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
|
|
61
61
|
const [agentName, setAgentName] = useState('');
|
|
62
62
|
const [portRange, setPortRange] = useState<string>('');
|
|
63
|
+
// The server render seeds this; the same endpoint the Edit panel uses refreshes it, so
|
|
64
|
+
// both flows validate against the same numbers (every container, running or stopped).
|
|
65
|
+
const [usedPorts, setUsedPorts] = useState<number[]>(initialUsedPorts);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
fetch('/api/agents/ports', { signal: controller.signal })
|
|
70
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
71
|
+
.then((data: { ports?: number[] } | null) => {
|
|
72
|
+
if (data && Array.isArray(data.ports)) setUsedPorts(data.ports);
|
|
73
|
+
})
|
|
74
|
+
.catch(() => { /* the server still validates on submit */ });
|
|
75
|
+
return () => controller.abort();
|
|
76
|
+
}, []);
|
|
63
77
|
const [selectedModel, setSelectedModel] = useState<string>(
|
|
64
78
|
models.length > 0 ? models[0].id : ''
|
|
65
79
|
);
|
|
@@ -73,11 +87,11 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
73
87
|
setSelectedTemplate(id);
|
|
74
88
|
setAgentName('');
|
|
75
89
|
// Auto-assign port block: find first free 10-port block
|
|
76
|
-
if (
|
|
90
|
+
if (usedPorts.length === 0) {
|
|
77
91
|
setPortRange('3700-3709');
|
|
78
92
|
} else {
|
|
79
|
-
//
|
|
80
|
-
const block =
|
|
93
|
+
// Same block finder as the server: the first free 10-port block.
|
|
94
|
+
const block = findAvailablePortBlock(new Set(usedPorts));
|
|
81
95
|
setPortRange(block ? `${block}-${block + 9}` : '');
|
|
82
96
|
}
|
|
83
97
|
};
|
|
@@ -91,9 +105,9 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
91
105
|
|
|
92
106
|
const portError = useMemo(() => {
|
|
93
107
|
if (!portRange.trim()) return '';
|
|
94
|
-
const res = validatePortInput(portRange, new Set(
|
|
108
|
+
const res = validatePortInput(portRange, new Set(usedPorts));
|
|
95
109
|
return res.valid ? '' : res.error;
|
|
96
|
-
}, [portRange,
|
|
110
|
+
}, [portRange, usedPorts]);
|
|
97
111
|
|
|
98
112
|
const canProceed = (): boolean => {
|
|
99
113
|
if (step === 'template') return selectedTemplate !== '';
|
|
@@ -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';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
3
|
+
import { getUsedHostPorts } from '@/lib/agent-ports-server';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/agents/ports
|
|
9
|
+
*
|
|
10
|
+
* The host ports held by any container, running or stopped — the set the port validators
|
|
11
|
+
* in lib/agent-ports.ts take. The create form receives it as a page prop; the Edit panel
|
|
12
|
+
* fetches it when it opens, so both flows validate a range against the same numbers and
|
|
13
|
+
* the same rules, and give the same message.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: Request): Promise<NextResponse> {
|
|
16
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
17
|
+
const ports = await getUsedHostPorts();
|
|
18
|
+
return NextResponse.json({ ports: [...ports].sort((a, b) => a - b) });
|
|
19
|
+
}
|
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);
|
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
|
|
@@ -189,6 +193,10 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
189
193
|
`recreateAgentContainer()` refuses such a clash **before** removing anything, naming the
|
|
190
194
|
container that holds each port, so the agent stays up and the operator changes the range
|
|
191
195
|
from the Edit panel; a failed `docker run` no longer leaves a container behind.
|
|
196
|
+
The two forms validate a range **as it is typed** against that same set — the create
|
|
197
|
+
wizard through its page prop plus `GET /api/agents/ports`, the Edit panel through the
|
|
198
|
+
endpoint when it opens — so both show the same message and disable their action while
|
|
199
|
+
the range is refused, instead of letting the server refuse it on submit.
|
|
192
200
|
- **Agent edits** are `lib/agent-edit.ts`, with their state in the `agent_edits` table
|
|
193
201
|
(`lib/agent-edit-state.ts`): the display name (`AGENT_NAME`) and/or the port range are
|
|
194
202
|
applied by rebuilding the container on the image it already runs — **no backup**, the
|
|
@@ -205,9 +213,9 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
205
213
|
`openclaw devices list | approve | reject | rename | remove` and
|
|
206
214
|
`openclaw dashboard --json`, run inside the container with the async `dockerExec`.
|
|
207
215
|
The gateway token is resolved inside the container (`/root/.agent-token`, then its
|
|
208
|
-
environment), never passed in argv.
|
|
209
|
-
|
|
210
|
-
|
|
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.
|
|
211
219
|
`buildInviteLink()` builds the plain token link for someone else; the invite route
|
|
212
220
|
refuses agents without approval. Every link's token is read inside the container,
|
|
213
221
|
and its host is the request's `Host` header (`hostnameFromHostHeader()`), never a
|
|
@@ -48,7 +48,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
48
48
|
| `ImageDownloadBanner` | `app/agents/ImageDownloadBanner.tsx` | Agent image banner on the Agents page. Polls `/api/agents/image-status` every 2 s; offers **Download Image** when no supported version is downloaded, **Download <version>** when the registry publishes a newer one, and shows the download in progress and its completion. Downloading changes no agent. |
|
|
49
49
|
| `BrowserAccessSection` / `OpenControlUiButton` | `app/agents/BrowserAccessSection.tsx` | Browser access to one agent's Control UI, in its detail panel: requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), refreshed every 5 s while mounted. A successful approve, reject, rename or revoke updates the list at once, since the refresh behind it runs the OpenClaw CLI and takes seconds; a read started before the mutation is discarded. On agents that require approval, "Invite link" fetches `/api/agents/[id]/invite-link` and shows the link in a read-only field with Copy, which uses the Clipboard API in a secure context and the field's selection over plain HTTP, plus a warning when the link uses localhost. `OpenControlUiButton` opens `/api/agents/[id]/open-control-ui` in a new tab inside the click; that route redirects to a one-time link that pairs the browser with no approval, or to the plain token link when none can be issued. Used on the agent cards and in the panel. |
|
|
50
50
|
| `ChannelManager` / `ChannelSection` | `app/agents/ChannelManager.tsx`, `ChannelSection` inline in `app/agents/PageClient.tsx` | Telegram, in the agent detail panel (`ChannelSection` is the card that opens the modal; the modal title is the agent's display name). Reads `GET /channels`; lists pending pairing requests with **Approve** (`POST /channels/pairing`) and approved senders with **Revoke** after a confirm (`DELETE /channels/pairing?senderId=`). Pending comes from `openclaw pairing list`, approved from OpenClaw's pairing store (`lib/channelManager.ts`). Polls pairings every 5 s while open; one keyed busy state per action. |
|
|
51
|
-
| `EditAgentModal` / `EditBanner` | `app/agents/PageClient.tsx` | Rename/ports editing. The modal has **Display Name** and **Host Port** (same input and validation messages as the create wizard, from `lib/agent-ports.ts`), sends `PATCH /api/agents/[id]` and closes on `202`; `EditBanner` (top of the agent detail) polls `GET /api/agents/[id]` every 2 s while an edit is `rebuilding`, resumes on mount, and refetches the agent once it finishes, so a reload or a navigation shows the running edit instead of allowing a second (the server answers 409). No backup is taken: the volume is untouched. |
|
|
51
|
+
| `EditAgentModal` / `EditBanner` | `app/agents/PageClient.tsx` | Rename/ports editing. The modal has **Display Name** and **Host Port** (same input and validation messages as the create wizard, from `lib/agent-ports.ts`), sends `PATCH /api/agents/[id]` and closes on `202`; `EditBanner` (top of the agent detail) polls `GET /api/agents/[id]` every 2 s while an edit is `rebuilding`, resumes on mount, and refetches the agent once it finishes, so a reload or a navigation shows the running edit instead of allowing a second (the server answers 409). No backup is taken: the volume is untouched. The port range is validated **as it is typed**: the modal fetches `GET /api/agents/ports` when it opens (the same set the create form uses, every container, running or stopped) and excludes the agent's own block, so an occupied range shows the API's message inline and the Save button stays disabled — the create wizard does the same with the same endpoint. |
|
|
52
52
|
| `ModelSection` | `app/agents/ModelSection.tsx` | Primary model and fallbacks for one agent, in its detail panel. Explicit save, no restart. The model is a property of the agent, not of the gateway. Tags models the catalogue marks `deprecated`. |
|
|
53
53
|
| `ModelsProvider` / `useModels` | `app/lib/models-context.tsx` | The client's single model list, from `/api/models`. Whatever changes what is offered calls `refresh()`: the Gateway page after a toggle, a key save or removal, or a sync, and the first-run wizard after saving keys. Everything else only reads, PulseChat included. |
|
|
54
54
|
| `WizardPageClient` + step components | `app/wizard/PageClient.tsx` | Multi-step first-run wizard plus its frame shell, with mobile-first CSS. |
|
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
|
|
@@ -704,6 +704,27 @@ List running agent containers (Docker containers with `AGENT_ID` label) with ful
|
|
|
704
704
|
|
|
705
705
|
---
|
|
706
706
|
|
|
707
|
+
### `GET /api/agents/ports`
|
|
708
|
+
The host ports held by any container, running or stopped: the set the validators in
|
|
709
|
+
`lib/agent-ports.ts` take, sorted ascending. Backed by the shared lookup
|
|
710
|
+
(`getUsedHostPorts()`, `lib/agent-ports-server.ts`), which reads every container's
|
|
711
|
+
`HostConfig.PortBindings` — a stopped container keeps its block while `docker ps -a` and
|
|
712
|
+
the API's container list report nothing for it.
|
|
713
|
+
|
|
714
|
+
**Auth:** `rev4a_token` cookie or `Authorization: Bearer <REV4A_TOKEN>`
|
|
715
|
+
|
|
716
|
+
**Response:**
|
|
717
|
+
```json
|
|
718
|
+
{ "ports": [3700, 3701, 3710, 3711, 3741, 5555] }
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
The Edit panel fetches it when it opens and the create form refreshes it on mount (the
|
|
722
|
+
page also receives it as a prop), so both validate a range as the operator types, against
|
|
723
|
+
the same numbers and with the same messages, and both disable their action while the range
|
|
724
|
+
is refused.
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
707
728
|
### `GET /api/agents/models-summary`
|
|
708
729
|
One row per agent container, running or not: the model it runs and whether that
|
|
709
730
|
model is still in good standing. Feeds the model chip on the agent cards. Running
|
|
@@ -1127,7 +1148,7 @@ agent was never recreated).
|
|
|
1127
1148
|
{
|
|
1128
1149
|
"recreate": {
|
|
1129
1150
|
"id": 12, "agentId": "agent_9253eee3", "status": "backing_up",
|
|
1130
|
-
"image": "openclaw-agent-base:2026.
|
|
1151
|
+
"image": "openclaw-agent-base:2026.9.3",
|
|
1131
1152
|
"backupFile": "agent-agent_9253eee3-prerecreate-2026-09-17_171000.tar.gz",
|
|
1132
1153
|
"backupPercent": 40, "error": null,
|
|
1133
1154
|
"startedAtMs": 1789656372120, "finishedAtMs": null
|
|
@@ -1149,12 +1170,12 @@ The agent's OpenClaw version, the update it could take, and its latest update
|
|
|
1149
1170
|
|
|
1150
1171
|
```json
|
|
1151
1172
|
{
|
|
1152
|
-
"currentVersion": "2026.
|
|
1153
|
-
"candidate": { "fromVersion": "2026.
|
|
1173
|
+
"currentVersion": "2026.9.3",
|
|
1174
|
+
"candidate": { "fromVersion": "2026.9.3", "toVersion": "2026.9.4" },
|
|
1154
1175
|
"update": {
|
|
1155
1176
|
"id": 3, "agentId": "agent_9253eee3", "status": "backing_up",
|
|
1156
|
-
"fromVersion": "2026.
|
|
1157
|
-
"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",
|
|
1158
1179
|
"backupPercent": 40, "verification": null, "error": null,
|
|
1159
1180
|
"startedAtMs": 1789524912000, "finishedAtMs": null
|
|
1160
1181
|
}
|
|
@@ -1176,9 +1197,10 @@ the background.
|
|
|
1176
1197
|
|
|
1177
1198
|
1. **Preflight** (refused with `409`): the agent runs, the target is downloaded,
|
|
1178
1199
|
supported and newer than the agent's version, no update or backup is running.
|
|
1179
|
-
2. **Baseline**, inside the agent: transcript events per session (
|
|
1180
|
-
|
|
1181
|
-
|
|
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.
|
|
1182
1204
|
3. **`backing_up`**: cold backup `agent-<id>-preupdate-<from>-<ts>.tar.gz`
|
|
1183
1205
|
(`startColdBackup`, agent left stopped).
|
|
1184
1206
|
4. **`migrating`**: the container is recreated on `openclaw-agent-base:<to>`
|
|
@@ -1363,8 +1385,8 @@ There is no CLI or RPC for this (`openclaw pairing` covers pending requests only
|
|
|
1363
1385
|
`channels.pairing.*` has no remove — verified on 2026.9.3). The pairing store's own
|
|
1364
1386
|
writer is used instead: `removeChannelAllowFromStoreEntry`, located by function name in
|
|
1365
1387
|
OpenClaw's `pairing-store` module and run inside the container, so the write goes
|
|
1366
|
-
through the same state transaction the CLI uses.
|
|
1367
|
-
|
|
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
|
|
1368
1390
|
fails with a message pointing at `/allowlist remove` from the chat — nothing is silently
|
|
1369
1391
|
left unchanged.
|
|
1370
1392
|
|
|
@@ -1391,9 +1413,8 @@ already approved.
|
|
|
1391
1413
|
**Auth:** browser cookie
|
|
1392
1414
|
|
|
1393
1415
|
Runs `openclaw devices list --json` inside the container with the async `dockerExec`;
|
|
1394
|
-
see `lib/agent-devices.ts`.
|
|
1395
|
-
and
|
|
1396
|
-
empty.
|
|
1416
|
+
see `lib/agent-devices.ts`. When the Gateway cannot be read `requiresApproval` is null
|
|
1417
|
+
and both lists are empty.
|
|
1397
1418
|
|
|
1398
1419
|
**Response:**
|
|
1399
1420
|
```json
|
|
@@ -1494,12 +1515,12 @@ Control UI.
|
|
|
1494
1515
|
The target host is the one the request reached Rev4a on (the `Host` header), with the
|
|
1495
1516
|
agent's published port — never a parameter, so the link cannot be sent to another host.
|
|
1496
1517
|
|
|
1497
|
-
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
-
|
|
1502
|
-
|
|
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
|
|
1503
1524
|
(`/root/.agent-token`, then `OPENCLAW_GATEWAY_TOKEN`).
|
|
1504
1525
|
|
|
1505
1526
|
A navigation, not a JSON call: the button opens this URL inside the click, so no tab
|
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;
|