@flame0510/project-aether 1.3.0 → 1.4.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/README.md +1 -0
- package/agent-templates/README.md +42 -22
- package/agent-templates/base-image/Dockerfile +42 -33
- package/agent-templates/base-image/entrypoint.sh +67 -12
- package/app/agents/BrowserAccessSection.tsx +510 -0
- package/app/agents/ChannelManager.tsx +19 -11
- package/app/agents/ImageDownloadBanner.tsx +53 -19
- package/app/agents/ModelSection.tsx +4 -1
- package/app/agents/PageClient.tsx +629 -167
- package/app/agents/UpdateSection.tsx +300 -0
- package/app/agents/create/PageClient.tsx +11 -49
- package/app/api/agents/[id]/backup/route.ts +26 -69
- package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
- package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
- package/app/api/agents/[id]/cold-backup/route.ts +56 -0
- package/app/api/agents/[id]/devices/route.ts +126 -0
- package/app/api/agents/[id]/invite-link/route.ts +53 -0
- package/app/api/agents/[id]/lifecycle/route.ts +3 -0
- package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
- package/app/api/agents/[id]/recreate/route.ts +33 -163
- package/app/api/agents/[id]/restart/route.ts +5 -0
- package/app/api/agents/[id]/restore/route.ts +40 -70
- package/app/api/agents/[id]/route.ts +38 -150
- package/app/api/agents/[id]/update/rollback/route.ts +30 -0
- package/app/api/agents/[id]/update/route.ts +50 -0
- package/app/api/agents/activity-summary/route.ts +67 -0
- package/app/api/agents/create/route.ts +32 -88
- package/app/api/agents/devices-summary/route.ts +37 -0
- package/app/api/agents/download-image/route.ts +16 -9
- package/app/api/agents/image-status/route.ts +31 -111
- package/app/api/agents/route.ts +25 -49
- package/app/api/agents/token/route.ts +33 -10
- package/app/api/assistant/route.ts +2 -2
- package/app/api/gateway/agent/route.ts +14 -0
- package/app/api/gateway/provider/balance/route.ts +5 -2
- package/app/api/gateway/sync.ts +97 -14
- package/app/api/setup/agent-image/route.ts +14 -42
- package/app/components/DashboardToolbar.tsx +1 -1
- package/app/gateway/PageClient.tsx +27 -32
- package/bin/rev4a.js +43 -41
- package/daemon.js +6 -6
- package/docs/ARCHITECTURE.md +95 -9
- package/docs/FRONTEND-ARCHITECTURE.md +8 -1
- package/docs/REV4A.md +54 -17
- package/docs/dev/API-REFERENCE.md +554 -100
- package/docs/dev/DATABASE.md +96 -0
- package/docs/dev/GATEWAY.md +21 -6
- package/docs/rag/DATA-FRESHNESS.md +6 -4
- package/docs/rag/GLOSSARY.md +12 -3
- package/docs/rag/REV4A-OVERVIEW.md +18 -5
- package/docs/rag/WHAT-I-CAN-ANSWER.md +6 -2
- package/instrumentation.ts +43 -0
- package/lib/agent-busy.ts +21 -0
- package/lib/agent-devices.ts +361 -0
- package/lib/agent-edit-state.ts +108 -0
- package/lib/agent-edit.ts +157 -0
- package/lib/agent-images.ts +375 -0
- package/lib/agent-ports-server.ts +27 -0
- package/lib/agent-ports.ts +68 -0
- package/lib/agent-recreate-state.ts +108 -0
- package/lib/agent-recreate.ts +305 -0
- package/lib/agent-restore-state.ts +107 -0
- package/lib/agent-restore.ts +135 -0
- package/lib/agent-setup.ts +66 -17
- package/lib/agent-update-state.ts +122 -0
- package/lib/agent-update.ts +448 -0
- package/lib/agent-versions.json +14 -0
- package/lib/agent-versions.ts +80 -0
- package/lib/buildAgentImage.ts +88 -290
- package/lib/channelManager.ts +149 -102
- package/lib/cold-backup.ts +354 -0
- package/lib/credentials/delivery.ts +3 -3
- package/lib/db-bootstrap.mjs +76 -0
- package/lib/docker-utils.ts +3 -3
- package/lib/provider-balance.ts +33 -12
- package/package.json +1 -1
|
@@ -1,60 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GET /api/setup/agent-image
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Returns
|
|
4
|
+
* Whether a supported OpenClaw version of the agent base image is downloaded.
|
|
5
|
+
* Returns `{ exists, version }`, the version being the newest one present.
|
|
6
6
|
*
|
|
7
7
|
* POST /api/setup/agent-image
|
|
8
8
|
*
|
|
9
|
-
* Downloads
|
|
10
|
-
*
|
|
9
|
+
* Downloads the newest supported version the registry publishes, and streams
|
|
10
|
+
* progress via SSE so the setup wizard can show a live progress bar. Nothing is
|
|
11
|
+
* downloaded when that version is already here.
|
|
11
12
|
*
|
|
12
13
|
* Only one download at a time — returns 409 if already running.
|
|
13
|
-
* Cleans up the docker process if the client disconnects.
|
|
14
14
|
*/
|
|
15
15
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
16
16
|
import { downloadAgentImage, getIsDownloading } from '@/lib/buildAgentImage';
|
|
17
|
-
import { execSync } from 'child_process';
|
|
18
17
|
import { requireAuthIfConfigured } from '@/lib/rev4a-auth';
|
|
18
|
+
import { dockerAvailable } from '@/lib/docker-socket';
|
|
19
|
+
import { listLocalAgentImages, newestLocalSupportedVersion } from '@/lib/agent-images';
|
|
19
20
|
|
|
20
21
|
export const dynamic = 'force-dynamic';
|
|
21
22
|
|
|
22
|
-
function dockerOk(): boolean {
|
|
23
|
-
try {
|
|
24
|
-
execSync('docker version --format "{{.Client.Version}}" 2>/dev/null', {
|
|
25
|
-
encoding: 'utf-8',
|
|
26
|
-
timeout: 2000,
|
|
27
|
-
});
|
|
28
|
-
return true;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
35
24
|
const denied = await requireAuthIfConfigured(request);
|
|
36
25
|
if (denied) return denied;
|
|
37
|
-
if (!
|
|
38
|
-
return NextResponse.json({ exists: false });
|
|
26
|
+
if (!dockerAvailable()) {
|
|
27
|
+
return NextResponse.json({ exists: false, version: null });
|
|
39
28
|
}
|
|
40
29
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
exec(
|
|
44
|
-
'docker image inspect openclaw-agent-base:latest --format "{{.ID}}" 2>/dev/null',
|
|
45
|
-
{ timeout: 5000 },
|
|
46
|
-
(err, stdout) => {
|
|
47
|
-
if (err) return reject(err);
|
|
48
|
-
resolve(stdout.trim());
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
if (result) {
|
|
53
|
-
return NextResponse.json({ exists: true, imageId: result });
|
|
54
|
-
}
|
|
55
|
-
return NextResponse.json({ exists: false });
|
|
30
|
+
const version = newestLocalSupportedVersion(await listLocalAgentImages());
|
|
31
|
+
return NextResponse.json({ exists: version !== null, version });
|
|
56
32
|
} catch {
|
|
57
|
-
return NextResponse.json({ exists: false });
|
|
33
|
+
return NextResponse.json({ exists: false, version: null });
|
|
58
34
|
}
|
|
59
35
|
}
|
|
60
36
|
|
|
@@ -81,11 +57,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
|
|
81
57
|
const ctrl = controller as ReadableStreamDefaultController & { signal?: AbortSignal };
|
|
82
58
|
|
|
83
59
|
try {
|
|
84
|
-
await downloadAgentImage({
|
|
85
|
-
force: false, // setup: skip if already exists
|
|
86
|
-
onEvent: send,
|
|
87
|
-
signal: ctrl.signal,
|
|
88
|
-
});
|
|
60
|
+
await downloadAgentImage({ onEvent: send, signal: ctrl.signal });
|
|
89
61
|
} catch (e: unknown) {
|
|
90
62
|
send(JSON.stringify({
|
|
91
63
|
type: 'error',
|
|
@@ -94,7 +66,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
|
|
94
66
|
}
|
|
95
67
|
controller.close();
|
|
96
68
|
},
|
|
97
|
-
cancel() { /* downloadAgentImage
|
|
69
|
+
cancel() { /* downloadAgentImage stops the docker process through the signal */ },
|
|
98
70
|
});
|
|
99
71
|
|
|
100
72
|
return new Response(stream, {
|
|
@@ -32,7 +32,7 @@ export default function DashboardToolbar({ filter, onChange, agents = ['all'] }:
|
|
|
32
32
|
className={`agent-btn ${filter.showOnlyActive ? 'active' : ''}`}
|
|
33
33
|
onClick={() => onChange({ showOnlyActive: !filter.showOnlyActive })}
|
|
34
34
|
>
|
|
35
|
-
|
|
35
|
+
active only
|
|
36
36
|
</button>
|
|
37
37
|
|
|
38
38
|
<button
|
|
@@ -88,7 +88,7 @@ export default function GatewayPageClient() {
|
|
|
88
88
|
const [modelSearch, setModelSearch] = useState<Record<string, string>>({});
|
|
89
89
|
|
|
90
90
|
// Balance fetch state (per provider)
|
|
91
|
-
const [balances, setBalances] = useState<Record<string, { balance: string; label?: string; loading?: boolean; error?: string } | null>>({});
|
|
91
|
+
const [balances, setBalances] = useState<Record<string, { balance: string; label?: string; loading?: boolean; error?: string; unsupported?: boolean } | null>>({});
|
|
92
92
|
const [fetchingBalance, setFetchingBalance] = useState<Record<string, boolean>>({});
|
|
93
93
|
|
|
94
94
|
// Accordion state — which provider cards are expanded
|
|
@@ -135,6 +135,9 @@ export default function GatewayPageClient() {
|
|
|
135
135
|
const bJson = await bRes.json();
|
|
136
136
|
if (bRes.ok && bJson.balance) {
|
|
137
137
|
setBalances((prev) => ({ ...prev, [bp]: { balance: bJson.balance, label: bJson.label } }));
|
|
138
|
+
} else if (bJson.reason === 'no-balance-api') {
|
|
139
|
+
// Permanent: this provider has no balance API for the account — hide the badge.
|
|
140
|
+
setBalances((prev) => ({ ...prev, [bp]: { balance: '', unsupported: true } }));
|
|
138
141
|
} else {
|
|
139
142
|
setBalances((prev) => ({ ...prev, [bp]: { balance: '—', error: bJson.error || 'Failed' } }));
|
|
140
143
|
}
|
|
@@ -198,6 +201,8 @@ export default function GatewayPageClient() {
|
|
|
198
201
|
const json = await res.json();
|
|
199
202
|
if (res.ok && json.balance) {
|
|
200
203
|
setBalances((prev) => ({ ...prev, [provider]: { balance: json.balance, label: json.label } }));
|
|
204
|
+
} else if (json.reason === 'no-balance-api') {
|
|
205
|
+
setBalances((prev) => ({ ...prev, [provider]: { balance: '', unsupported: true } }));
|
|
201
206
|
} else {
|
|
202
207
|
setBalances((prev) => ({ ...prev, [provider]: { balance: '—', error: json.error || 'Failed' } }));
|
|
203
208
|
}
|
|
@@ -519,43 +524,33 @@ export default function GatewayPageClient() {
|
|
|
519
524
|
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-dim)' }}>
|
|
520
525
|
{p.models.length} models
|
|
521
526
|
</span>
|
|
522
|
-
{/* Balance badge — auto-loaded for supported providers with configured key
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
style={{
|
|
543
|
-
fontSize: 11,
|
|
544
|
-
color: 'var(--text-dim)',
|
|
545
|
-
opacity: 0.5,
|
|
546
|
-
cursor: 'pointer',
|
|
547
|
-
}}
|
|
548
|
-
>
|
|
549
|
-
{fetchingBalance[p.provider] ? '…' : '…'}
|
|
550
|
-
</span>
|
|
551
|
-
)
|
|
527
|
+
{/* Balance badge — auto-loaded for supported providers with configured key.
|
|
528
|
+
Same squared chip in every state: "..." while loading, "—" (dimmed, tooltip) on error.
|
|
529
|
+
Hidden entirely when the provider has no balance API for the account (permanent). */}
|
|
530
|
+
{p.configured && ['deepseek','openrouter','glm'].includes(p.provider) && !balances[p.provider]?.unsupported && (
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
title={balances[p.provider]?.error || balances[p.provider]?.label || ''}
|
|
534
|
+
onClick={(e) => { e.stopPropagation(); fetchBalance(p.provider); }}
|
|
535
|
+
style={{
|
|
536
|
+
fontSize: 11, fontWeight: 600,
|
|
537
|
+
color: 'var(--green)',
|
|
538
|
+
background: 'rgba(var(--green-rgb,34,197,94),0.1)',
|
|
539
|
+
padding: '2px 8px', borderRadius: 0,
|
|
540
|
+
border: '1px solid rgba(var(--green-rgb,34,197,94),0.2)',
|
|
541
|
+
cursor: 'pointer',
|
|
542
|
+
opacity: balances[p.provider]?.error ? 0.5 : 1,
|
|
543
|
+
}}
|
|
544
|
+
>
|
|
545
|
+
{balances[p.provider] && !balances[p.provider]?.error ? balances[p.provider]!.balance : balances[p.provider]?.error ? '—' : '...'}
|
|
546
|
+
</button>
|
|
552
547
|
)}
|
|
553
548
|
</div>
|
|
554
549
|
|
|
555
550
|
{/* Accordion body — expandable */}
|
|
556
551
|
{isExpanded && (
|
|
557
552
|
<div style={{ padding: '0 18px 16px 18px', display: 'grid', gap: 14, borderTop: '1px solid var(--border)', paddingTop: 14 }}>
|
|
558
|
-
{/* API Key input
|
|
553
|
+
{/* API Key input with an inline show/hide toggle */}
|
|
559
554
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
560
555
|
<PasswordInput
|
|
561
556
|
value={apiKeyInputs[p.provider] ?? ''}
|
package/bin/rev4a.js
CHANGED
|
@@ -41,8 +41,8 @@ const DATA_DIR = join(CONFIG_DIR, 'data');
|
|
|
41
41
|
const ENV_SOURCE = join(CONFIG_DIR, '.env');
|
|
42
42
|
const ENV_LINK = join(ROOT, '.env');
|
|
43
43
|
const DOCKERFILE = join(ROOT, 'agent-templates', 'base-image', 'Dockerfile');
|
|
44
|
-
|
|
45
|
-
const
|
|
44
|
+
// Supported OpenClaw versions and image repositories, shared with the server.
|
|
45
|
+
const AGENT_VERSIONS = require(join(ROOT, 'lib', 'agent-versions.json'));
|
|
46
46
|
const DOCKER_NETWORK = 'rev4a-network';
|
|
47
47
|
// How long `serve` waits for the Docker daemon before starting without it. Like
|
|
48
48
|
// REV4A_DATA_DIR it is read from the real process environment (a systemd
|
|
@@ -204,55 +204,57 @@ function ensureDocker() {
|
|
|
204
204
|
function ensureAgentImage() {
|
|
205
205
|
if (!hasCmd('docker')) return; // Docker not available — skip, warn later
|
|
206
206
|
|
|
207
|
-
//
|
|
207
|
+
// The newest OpenClaw version this release supports, kept under its version tag.
|
|
208
|
+
// `:latest` is never used: an agent's version decides its data schema.
|
|
209
|
+
const version = AGENT_VERSIONS.supported[0].version;
|
|
210
|
+
const localRef = `${AGENT_VERSIONS.localRepository}:${version}`;
|
|
211
|
+
const registry = (process.env.REV4A_AGENT_IMAGE_REGISTRY || '').trim() || AGENT_VERSIONS.registryRepository;
|
|
212
|
+
const remoteRef = `${registry}:${version}`;
|
|
213
|
+
|
|
208
214
|
try {
|
|
209
|
-
const r = execSync(
|
|
210
|
-
`docker image inspect ${AGENT_IMAGE} --format '{{.Id}}'`,
|
|
211
|
-
{ stdio: 'pipe', timeout: 10_000 },
|
|
212
|
-
);
|
|
215
|
+
const r = execSync(`docker image inspect ${localRef} --format '{{.Id}}'`, { stdio: 'pipe', timeout: 10_000 });
|
|
213
216
|
if (r.toString().trim()) {
|
|
214
|
-
log('IMAGE', `Agent base image ${
|
|
217
|
+
log('IMAGE', `Agent base image ${localRef} already exists`);
|
|
215
218
|
return;
|
|
216
219
|
}
|
|
217
220
|
} catch {
|
|
218
|
-
//
|
|
221
|
+
// Not downloaded yet
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
log('IMAGE', `Pulling agent base image
|
|
222
|
-
|
|
224
|
+
log('IMAGE', `Pulling agent base image ${remoteRef}…`);
|
|
223
225
|
try {
|
|
224
|
-
execSync(`docker pull ${
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
timeout: 10_000,
|
|
232
|
-
});
|
|
233
|
-
log('IMAGE', `Agent base image pulled and tagged as ${AGENT_IMAGE}`);
|
|
226
|
+
execSync(`docker pull ${remoteRef}`, { stdio: 'inherit', timeout: 1_800_000 });
|
|
227
|
+
execSync(`docker tag ${remoteRef} ${localRef}`, { stdio: 'inherit', timeout: 10_000 });
|
|
228
|
+
try {
|
|
229
|
+
execSync(`docker rmi ${remoteRef}`, { stdio: 'pipe', timeout: 30_000 }); // untag; the layers stay
|
|
230
|
+
} catch { /* keeping the registry reference is harmless */ }
|
|
231
|
+
log('IMAGE', `Agent base image pulled and tagged as ${localRef}`);
|
|
232
|
+
return;
|
|
234
233
|
} catch {
|
|
235
|
-
log('IMAGE',
|
|
236
|
-
|
|
237
|
-
if (!existsSync(DOCKERFILE)) {
|
|
238
|
-
log('IMAGE', `⚠ Agent base image Dockerfile not found at ${DOCKERFILE}`);
|
|
239
|
-
log('IMAGE', ' Agent creation will be unavailable until the image is available.');
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
234
|
+
log('IMAGE', '⚠ Pull from registry failed');
|
|
235
|
+
}
|
|
242
236
|
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
// A local build is only the same image when this Dockerfile builds that version.
|
|
238
|
+
const dockerfileVersion = existsSync(DOCKERFILE)
|
|
239
|
+
? (/^ARG OPENCLAW_VERSION=(\S+)/m.exec(readFileSync(DOCKERFILE, 'utf-8')) || [])[1] || null
|
|
240
|
+
: null;
|
|
241
|
+
if (dockerfileVersion !== version) {
|
|
242
|
+
log('IMAGE', ` The Dockerfile here builds ${dockerfileVersion || 'no version'}, not ${version}: no local build.`);
|
|
243
|
+
log('IMAGE', ' Agent creation will be unavailable until the image is downloaded from the Agents page.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
245
246
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
247
|
+
log('IMAGE', `Building agent base image ${localRef} (this will take a few minutes)…`);
|
|
248
|
+
log('IMAGE', ' → pulling node:24-bookworm-slim + installing OpenClaw + tooling');
|
|
249
|
+
try {
|
|
250
|
+
execSync(
|
|
251
|
+
`docker build --build-arg OPENCLAW_VERSION=${version} -t ${localRef} -f "${DOCKERFILE}" "${join(DOCKERFILE, '..')}"`,
|
|
252
|
+
{ stdio: 'inherit', timeout: 3_600_000 },
|
|
253
|
+
);
|
|
254
|
+
log('IMAGE', 'Agent base image built successfully');
|
|
255
|
+
} catch {
|
|
256
|
+
log('IMAGE', '⚠ Agent base image build failed. Agent creation will be unavailable.');
|
|
257
|
+
log('IMAGE', ` Pull it manually: docker pull ${remoteRef} && docker tag ${remoteRef} ${localRef}`);
|
|
256
258
|
}
|
|
257
259
|
}
|
|
258
260
|
|
|
@@ -639,7 +641,7 @@ EXAMPLES:
|
|
|
639
641
|
On first run, rev4a serve automatically:
|
|
640
642
|
• Generates .env (JWT secret + API token)
|
|
641
643
|
• Installs Docker if missing (requires root)
|
|
642
|
-
•
|
|
644
|
+
• Downloads the agent base image for the newest supported OpenClaw version
|
|
643
645
|
• Builds the Next.js app if no build is found
|
|
644
646
|
• Starts Next.js, daemon, and terminal WebSocket as a single process group
|
|
645
647
|
• Cleanly stops all services on Ctrl+C / SIGTERM
|
package/daemon.js
CHANGED
|
@@ -668,12 +668,12 @@ function pollSessions() {
|
|
|
668
668
|
// ── Timeout Detection ────────────────────────────────────────────────────
|
|
669
669
|
// Find sub-agents that were 'working' in the last snapshot but are no longer
|
|
670
670
|
// present in the current output and have been missing for more than 10 minutes.
|
|
671
|
-
//
|
|
672
|
-
const TIMEOUT_THRESHOLD_MS = 10 * 60 * 1000; // 10
|
|
671
|
+
// These are recorded as 'timeout' in the events table.
|
|
672
|
+
const TIMEOUT_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
|
|
673
673
|
for (const [session_id, snap] of knownSessions) {
|
|
674
674
|
// Only sub-agents (contain 'subagent' in the key)
|
|
675
675
|
if (!session_id.includes('subagent')) continue;
|
|
676
|
-
//
|
|
676
|
+
// Only if they were working in the last poll
|
|
677
677
|
if (snap.status !== 'working') continue;
|
|
678
678
|
// Only if not present in the current poll
|
|
679
679
|
if (currentIds.has(session_id)) continue;
|
|
@@ -694,10 +694,10 @@ function pollSessions() {
|
|
|
694
694
|
data: JSON.stringify({
|
|
695
695
|
missing_for_ms: missingFor,
|
|
696
696
|
last_status: snap.status,
|
|
697
|
-
message: `
|
|
697
|
+
message: `Subagent missing for ${Math.round(missingFor / 60000)} min without completing`,
|
|
698
698
|
}),
|
|
699
699
|
});
|
|
700
|
-
log(`[TIMEOUT] ${session_id} missing
|
|
700
|
+
log(`[TIMEOUT] ${session_id} missing for ${Math.round(missingFor / 60000)} min`);
|
|
701
701
|
|
|
702
702
|
// Notify Michele via openclaw message (only if openclaw is available)
|
|
703
703
|
try {
|
|
@@ -708,7 +708,7 @@ function pollSessions() {
|
|
|
708
708
|
'message', 'send',
|
|
709
709
|
'--account', 'ops',
|
|
710
710
|
'--target', '297086793',
|
|
711
|
-
'--text', `⚠️ Rev4a: agent timeout\n\`${session_id.slice(-36)}\`\nMissing for ${Math.round(missingFor / 60000)} min
|
|
711
|
+
'--text', `⚠️ Rev4a: agent timeout\n\`${session_id.slice(-36)}\`\nMissing for ${Math.round(missingFor / 60000)} min without completing.`,
|
|
712
712
|
], { encoding: 'utf8', timeout: 10_000, killSignal: 'SIGKILL' });
|
|
713
713
|
} catch (e) {
|
|
714
714
|
log(`[TIMEOUT] Telegram notification failed: ${e.message}`);
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Rev4a Architecture — Design & Vision
|
|
2
2
|
|
|
3
3
|
> **Status:** Active — `main` branch
|
|
4
|
-
> **Last updated:** 2026-09-
|
|
4
|
+
> **Last updated:** 2026-09-15
|
|
5
5
|
> **Goal:** Transform Rev4a from a monitoring dashboard into a central orchestrator for a distributed multi-container agency.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -130,6 +130,89 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
130
130
|
a key), `isModelOffered()` (enforced by the proxy and the assistant) and
|
|
131
131
|
`catalogueStatus()`. A failed read of `models.config.json` falls back to the last
|
|
132
132
|
good copy and never deletes overrides.
|
|
133
|
+
- **Agent images and OpenClaw versions.** `lib/agent-versions.json` lists the supported
|
|
134
|
+
OpenClaw versions, newest first, with the model `input` list for each; the server reads
|
|
135
|
+
it through `lib/agent-versions.ts`, the `rev4a` CLI with `require`. The provider sync
|
|
136
|
+
takes each agent's own list (`containerVersionSync()`: image tag, else the entrypoint's
|
|
137
|
+
`.last-version` file), so a 2026.7.x agent is never sent an `input` value it would
|
|
138
|
+
reject — one unknown value discards the whole generated catalogue. The previous
|
|
139
|
+
versions stay listed while agents still run them: that is also what lets a rollback
|
|
140
|
+
pull the previous image. `lib/agent-images.ts`
|
|
141
|
+
lists local `openclaw-agent-base:<version>` images through the Docker API, reads an
|
|
142
|
+
agent's version (image label or tag, else `openclaw --version` in the container, cached
|
|
143
|
+
per image id), reads the registry tag list over HTTP (cached 10 min), pulls a version
|
|
144
|
+
and resolves the image a recreate uses. Images are never addressed as `:latest`: create
|
|
145
|
+
takes the newest supported version downloaded, recreate the agent's own.
|
|
146
|
+
- **Cold backups** are `lib/cold-backup.ts`. The agent is stopped and a detached helper
|
|
147
|
+
container, `rev4a-backup-<id>` from the agent's own image, archives its volume to
|
|
148
|
+
`rev4a-backups` as `.partial`, reads it back and renames it; the agent is started
|
|
149
|
+
again if it was running. The helper and its labels are the job, so no request waits
|
|
150
|
+
on it and a Rev4a restart does not lose it (`reconcileColdBackups()` in
|
|
151
|
+
`instrumentation.ts`). While a helper runs, the routes that change the agent answer
|
|
152
|
+
409 (`isColdBackupRunning()`), and `syncAllAgents()` skips the agent.
|
|
153
|
+
- **Agent updates** are `lib/agent-update.ts`, with their state in the `agent_upgrades`
|
|
154
|
+
table (`lib/agent-update-state.ts`). An update counts the agent's transcript events per
|
|
155
|
+
session and its cron jobs, takes a cold pre-update backup, recreates the container on
|
|
156
|
+
the newer version (`lib/agent-recreate.ts`, argv only, env values outside the process
|
|
157
|
+
table), waits for `/startupz` to report that version, re-applies Rev4a's config and
|
|
158
|
+
checks the counts. A rollback restores the pre-update backup on the previous version.
|
|
159
|
+
Both run inside the Rev4a process; `markInterruptedUpdates()` at startup turns an
|
|
160
|
+
unfinished one into `interrupted`. `lib/agent-busy.ts` gives the routes one 409 reason
|
|
161
|
+
for "edit, update, recreate, restore or backup running". After a successful update, `pruneAgentImages()`
|
|
162
|
+
keeps only images in use plus the newest and previous versions.
|
|
163
|
+
- **Agent recreates** are `lib/agent-recreate.ts`, with their state in the
|
|
164
|
+
`agent_recreates` table (`lib/agent-recreate-state.ts`): the same-version counterpart
|
|
165
|
+
of an update. A cold backup, then the container rebuilt on the image the agent already
|
|
166
|
+
runs, then `/startupz`, then the runtime config. It answers `202` and runs in the
|
|
167
|
+
background, so a page reload or a Rev4a restart does not lose it; at startup a job
|
|
168
|
+
still active becomes `interrupted` and the agent is started again — after its backup
|
|
169
|
+
helper has finished, never while the archive is written. On success the older
|
|
170
|
+
`prerecreate` archives are pruned to the newest two. The same `recreateAgentContainer()`
|
|
171
|
+
serves the edit route (`PATCH` → 202, no backup) and the Update action.
|
|
172
|
+
`agentBusyReason()` (`lib/agent-busy.ts`) covers it for every route that changes an agent.
|
|
173
|
+
- **Host ports** are shared between creation and the edit flow by `lib/agent-ports.ts`
|
|
174
|
+
(pure: `validatePortInput`, `findAvailablePortBlock`, `portMappingArgs`) and
|
|
175
|
+
`lib/agent-ports-server.ts` (`getUsedHostPorts()`, the Docker lookup, server-only since
|
|
176
|
+
client components cannot import `child_process`). One block maps to the container's
|
|
177
|
+
gateway port 3000 (`3700-3709` → `3700-3709:3000-3009`); both flows produce the same
|
|
178
|
+
validation messages, and the edit excludes the agent's own ports so its block never
|
|
179
|
+
conflicts with itself.
|
|
180
|
+
- **Agent edits** are `lib/agent-edit.ts`, with their state in the `agent_edits` table
|
|
181
|
+
(`lib/agent-edit-state.ts`): the display name (`AGENT_NAME`) and/or the port range are
|
|
182
|
+
applied by rebuilding the container on the image it already runs — **no backup**, the
|
|
183
|
+
volume is never touched. Same 202-and-background shape as a restore; the parameters
|
|
184
|
+
live in the row, so recovery can tell what was being applied. The panel shows a banner
|
|
185
|
+
and the agent card an `EDITING` chip.
|
|
186
|
+
- **Agent restores** are `lib/agent-restore.ts`, with their state in the `agent_restores`
|
|
187
|
+
table (`lib/agent-restore-state.ts`): the archive replaces the volume (stop, clear,
|
|
188
|
+
extract, start), up to 30 minutes. Same 202-and-background shape as a recreate, and the
|
|
189
|
+
same guarantees — a reload or a Rev4a restart does not lose the job, a second restore
|
|
190
|
+
is refused, an interrupted one starts the container again. The extract has no
|
|
191
|
+
percentage (it is a single `tar xzf`); the panel and the agent card show `RESTORING`.
|
|
192
|
+
- **Browser access** to an agent's Control UI goes through `lib/agent-devices.ts`:
|
|
193
|
+
`openclaw devices list | approve | reject | rename | remove` and
|
|
194
|
+
`openclaw dashboard --json`, run inside the container with the async `dockerExec`.
|
|
195
|
+
The gateway token is resolved inside the container (`/root/.agent-token`, then its
|
|
196
|
+
environment), never passed in argv. Whether approval applies is decided by the
|
|
197
|
+
OpenClaw version: 9.x answers `/startupz` with JSON and always requires it;
|
|
198
|
+
2026.7.x follows `gateway.controlUi.dangerouslyDisableDeviceAuth`.
|
|
199
|
+
`buildInviteLink()` builds the plain token link for someone else; the invite route
|
|
200
|
+
refuses agents without approval. Every link's token is read inside the container,
|
|
201
|
+
and its host is the request's `Host` header (`hostnameFromHostHeader()`), never a
|
|
202
|
+
caller-supplied value.
|
|
203
|
+
- **Telegram DM pairing** lives in `lib/channelManager.ts`. The bot binding is config
|
|
204
|
+
(`channels.telegram.botToken`/`dmPolicy`); *who* may talk is OpenClaw's pairing
|
|
205
|
+
store — on 2026.9.3 the SQLite rows in `~/.openclaw/state/openclaw.sqlite`, not the
|
|
206
|
+
`credentials/*.json` files older releases used. Reading and revoking go through the
|
|
207
|
+
store's own functions (`readChannelAllowFromStoreSync`,
|
|
208
|
+
`removeChannelAllowFromStoreEntry`): no CLI, RPC or documented export exposes them,
|
|
209
|
+
so a small script run inside the container finds OpenClaw's `pairing-store` module by
|
|
210
|
+
glob and its functions by name (the names survive minification; no hash or minified
|
|
211
|
+
symbol is hardcoded) and calls them, which uses the same state transaction the CLI
|
|
212
|
+
does. If a release stops exposing the store, the revoke fails with a message pointing
|
|
213
|
+
at `/allowlist remove` — it never reports success without changing anything. Approving
|
|
214
|
+
a pairing request (`openclaw pairing approve`) also bootstraps
|
|
215
|
+
`commands.ownerAllowFrom` for the first owner (OpenClaw's own behaviour).
|
|
133
216
|
|
|
134
217
|
**Running commands inside containers** (`lib/docker-exec.ts`):
|
|
135
218
|
|
|
@@ -149,6 +232,7 @@ the rules for anything you touch, not as a description of the whole tree:
|
|
|
149
232
|
container cannot blank an entire page. Callers: `app/api/skills/route.js`,
|
|
150
233
|
`app/api/agents/models-summary/route.ts`,
|
|
151
234
|
`app/api/agents/channels-summary/route.ts`,
|
|
235
|
+
`app/api/agents/devices-summary/route.ts`,
|
|
152
236
|
`lib/openclaw-cron.ts`, `app/api/credentials/detect/route.ts`,
|
|
153
237
|
`lib/credentials/delivery.ts`.
|
|
154
238
|
- **Never interpolate a secret into a command string.** Pass it through the `env`
|
|
@@ -195,7 +279,9 @@ agent-{name}-data (named vol) → /root (rw,
|
|
|
195
279
|
All shared volumes are defined centrally in `lib/agent-setup.ts`
|
|
196
280
|
(`REV4A_VOLUMES`) and used by both the create and recreate API routes.
|
|
197
281
|
The `bootstrap-extra-files` hook is pre-wired in the base image Dockerfile
|
|
198
|
-
and guaranteed at runtime by `applyRuntimeConfig()
|
|
282
|
+
and guaranteed at runtime by `applyRuntimeConfig()`, which also applies the Control
|
|
283
|
+
UI browser-origin policy (`withControlUiPolicy()`): the Host-header fallback on, an
|
|
284
|
+
`allowedOrigins: ["*"]` list and the retired `dangerouslyDisableDeviceAuth` removed.
|
|
199
285
|
|
|
200
286
|
**Injected environment variables:**
|
|
201
287
|
```
|
|
@@ -337,7 +423,7 @@ Rev4a Dashboard UI
|
|
|
337
423
|
POST /api/agents/create Create new container agent (template + model)
|
|
338
424
|
DELETE /api/agents/{id} Destructive: remove container + volume + backups
|
|
339
425
|
POST /api/agents/{id}/restart Restart container
|
|
340
|
-
POST /api/agents/{id}/recreate Rebuild
|
|
426
|
+
POST /api/agents/{id}/recreate Rebuild on the agent's own version (volume preserved)
|
|
341
427
|
GET /api/agents List all agents (Docker containers with AGENT_ID)
|
|
342
428
|
```
|
|
343
429
|
|
|
@@ -422,7 +508,7 @@ networks:
|
|
|
422
508
|
- Agent → Rev4a Gateway: `http://rev4a-control:3721`
|
|
423
509
|
- Agent → Rev4a Dashboard: `http://rev4a-control:3720`
|
|
424
510
|
- Rev4a → Agent (healthcheck): `http://agent-{id}:3000`
|
|
425
|
-
-
|
|
511
|
+
- Browser → Agent Control UI: the container's published host port, `http://<host>:<port>`. Rev4a sets no reverse-proxy labels
|
|
426
512
|
|
|
427
513
|
---
|
|
428
514
|
|
|
@@ -573,10 +659,10 @@ ALTER TABLE sessions ADD COLUMN ended_at INTEGER;
|
|
|
573
659
|
- Distributed: per-agent memory, Rev4a indexes centrally — more resilient
|
|
574
660
|
- Recommendation: distributed with central index
|
|
575
661
|
|
|
576
|
-
3. **
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
662
|
+
3. **External access to agents** — decided: each agent's Control UI is published on a
|
|
663
|
+
host port, and Rev4a sets no reverse-proxy labels. HTTPS or a domain means the
|
|
664
|
+
operator's own proxy in front of that port. A first-class integration, with routing
|
|
665
|
+
labels applied at `docker run`, would be a separate feature.
|
|
580
666
|
|
|
581
667
|
4. **Hermes and other frameworks: dashboard integration?**
|
|
582
668
|
- Hermes has its own session format
|
|
@@ -628,6 +714,6 @@ for the full rationale.
|
|
|
628
714
|
| Memory | Per-agent SQLite + central index | New |
|
|
629
715
|
| Config | Generated YAML/JSON | New |
|
|
630
716
|
| Credential stores | SQLite + JSON | credentials.db for service tokens, provider-keys.json for provider API keys. Neither is encrypted |
|
|
631
|
-
| Reverse proxy |
|
|
717
|
+
| Reverse proxy | Optional, operator's choice | Rev4a sets no reverse-proxy labels on agent containers |
|
|
632
718
|
| Monitoring | Rev4a daemon (extended) | Evolution of current daemon |
|
|
633
719
|
| Version control | Git + GitHub | `github.com/Flame0510/rev4a.git` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a Frontend Architecture
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-15
|
|
4
4
|
|
|
5
5
|
## Layering
|
|
6
6
|
|
|
@@ -42,6 +42,13 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
42
42
|
| `Page` / `PageHeader` | `Page.tsx` | Full-page layout shell. |
|
|
43
43
|
| `Icons` | `Icons.tsx` | SVG icons (`EyeIcon`, `EyeOffIcon`), 16/20px shared. |
|
|
44
44
|
| `PasswordInput` | `PasswordInput.tsx` | Password input with inline show/hide toggle (`<button type="button">` with `aria-label`). |
|
|
45
|
+
| `UpdateSection` | `app/agents/UpdateSection.tsx` | OPENCLAW VERSION section of the agent detail panel: the version the agent runs, **Update to <version>** when a newer supported version is downloaded (confirm modal, disabled while the agent is stopped), the running update's steps with backup progress, the outcome, and **Roll back to <version>** after an update. Polls `/api/agents/[id]/update` every 2 s while an update or rollback runs; one action at a time (keyed busy state). |
|
|
46
|
+
| `BackupSection` | inline in `app/agents/PageClient.tsx` | BACKUP section of the agent detail panel, on the cold backup and the restore. **Backup Now** starts `POST /api/agents/[id]/cold-backup`; while the job runs a banner shows the file and its live percent with **Cancel** (`DELETE /cold-backup`). **Restore** (after a confirm) starts `POST /restore` and a banner shows `Restoring <file>…` (no percent: the extract is a single `tar xzf`, and there is no Cancel). Both sections poll their `GET` every 2 s while running, and on mount pick up a job that is already running — a backup lives in a Docker helper, a restore in `agent_restores`, so reloading the page or navigating away never loses them nor allows a second one (the server answers 409 anyway). Delete per row; all actions disabled while one runs; keyed busy state `{ kind, file }` so only the row in action shows the spinner. On the agent list, an activity Badge (fed by `/api/agents/activity-summary`, polled at 2 s only while something runs, otherwise riding the 15 s list poll) reads `BACKUP nn%`, `RESTORING`, `RECREATING`, `EDITING` or `UPDATING`. |
|
|
47
|
+
| `RecreateSection` | inline in `app/agents/PageClient.tsx` | RECREATE section of the agent detail panel. **Recreate Container** starts `POST /api/agents/[id]/recreate` (202) after a confirm; a banner then shows the phase — *Backing up … nn%* while the cold backup runs, *Recreating container…* while the container is rebuilt and the gateway starts. The section polls `GET /recreate` every 2 s, and on mount picks up a recreate that is already running, so a reload or navigation never loses it; it refetches the agent once the job reports `done`. |
|
|
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
|
+
| `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
|
+
| `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. |
|
|
45
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`. |
|
|
46
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. |
|
|
47
54
|
| `WizardPageClient` + step components | `app/wizard/PageClient.tsx` | Multi-step first-run wizard plus its frame shell, with mobile-first CSS. |
|