@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.
Files changed (76) hide show
  1. package/README.md +1 -0
  2. package/agent-templates/README.md +42 -22
  3. package/agent-templates/base-image/Dockerfile +42 -33
  4. package/agent-templates/base-image/entrypoint.sh +67 -12
  5. package/app/agents/BrowserAccessSection.tsx +510 -0
  6. package/app/agents/ChannelManager.tsx +19 -11
  7. package/app/agents/ImageDownloadBanner.tsx +53 -19
  8. package/app/agents/ModelSection.tsx +4 -1
  9. package/app/agents/PageClient.tsx +629 -167
  10. package/app/agents/UpdateSection.tsx +300 -0
  11. package/app/agents/create/PageClient.tsx +11 -49
  12. package/app/api/agents/[id]/backup/route.ts +26 -69
  13. package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
  14. package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
  15. package/app/api/agents/[id]/cold-backup/route.ts +56 -0
  16. package/app/api/agents/[id]/devices/route.ts +126 -0
  17. package/app/api/agents/[id]/invite-link/route.ts +53 -0
  18. package/app/api/agents/[id]/lifecycle/route.ts +3 -0
  19. package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
  20. package/app/api/agents/[id]/recreate/route.ts +33 -163
  21. package/app/api/agents/[id]/restart/route.ts +5 -0
  22. package/app/api/agents/[id]/restore/route.ts +40 -70
  23. package/app/api/agents/[id]/route.ts +38 -150
  24. package/app/api/agents/[id]/update/rollback/route.ts +30 -0
  25. package/app/api/agents/[id]/update/route.ts +50 -0
  26. package/app/api/agents/activity-summary/route.ts +67 -0
  27. package/app/api/agents/create/route.ts +32 -88
  28. package/app/api/agents/devices-summary/route.ts +37 -0
  29. package/app/api/agents/download-image/route.ts +16 -9
  30. package/app/api/agents/image-status/route.ts +31 -111
  31. package/app/api/agents/route.ts +25 -49
  32. package/app/api/agents/token/route.ts +33 -10
  33. package/app/api/assistant/route.ts +2 -2
  34. package/app/api/gateway/agent/route.ts +14 -0
  35. package/app/api/gateway/provider/balance/route.ts +5 -2
  36. package/app/api/gateway/sync.ts +97 -14
  37. package/app/api/setup/agent-image/route.ts +14 -42
  38. package/app/components/DashboardToolbar.tsx +1 -1
  39. package/app/gateway/PageClient.tsx +27 -32
  40. package/bin/rev4a.js +43 -41
  41. package/daemon.js +6 -6
  42. package/docs/ARCHITECTURE.md +95 -9
  43. package/docs/FRONTEND-ARCHITECTURE.md +8 -1
  44. package/docs/REV4A.md +54 -17
  45. package/docs/dev/API-REFERENCE.md +554 -100
  46. package/docs/dev/DATABASE.md +96 -0
  47. package/docs/dev/GATEWAY.md +21 -6
  48. package/docs/rag/DATA-FRESHNESS.md +6 -4
  49. package/docs/rag/GLOSSARY.md +12 -3
  50. package/docs/rag/REV4A-OVERVIEW.md +18 -5
  51. package/docs/rag/WHAT-I-CAN-ANSWER.md +6 -2
  52. package/instrumentation.ts +43 -0
  53. package/lib/agent-busy.ts +21 -0
  54. package/lib/agent-devices.ts +361 -0
  55. package/lib/agent-edit-state.ts +108 -0
  56. package/lib/agent-edit.ts +157 -0
  57. package/lib/agent-images.ts +375 -0
  58. package/lib/agent-ports-server.ts +27 -0
  59. package/lib/agent-ports.ts +68 -0
  60. package/lib/agent-recreate-state.ts +108 -0
  61. package/lib/agent-recreate.ts +305 -0
  62. package/lib/agent-restore-state.ts +107 -0
  63. package/lib/agent-restore.ts +135 -0
  64. package/lib/agent-setup.ts +66 -17
  65. package/lib/agent-update-state.ts +122 -0
  66. package/lib/agent-update.ts +448 -0
  67. package/lib/agent-versions.json +14 -0
  68. package/lib/agent-versions.ts +80 -0
  69. package/lib/buildAgentImage.ts +88 -290
  70. package/lib/channelManager.ts +149 -102
  71. package/lib/cold-backup.ts +354 -0
  72. package/lib/credentials/delivery.ts +3 -3
  73. package/lib/db-bootstrap.mjs +76 -0
  74. package/lib/docker-utils.ts +3 -3
  75. package/lib/provider-balance.ts +33 -12
  76. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  # Rev4a API Reference
2
2
 
3
- > **Last updated:** 2026-09-14
3
+ > **Last updated:** 2026-09-15
4
4
 
5
5
  All routes are under `/api/`. Authentication is required on every endpoint
6
6
  unless otherwise noted.
@@ -124,15 +124,18 @@ Restart the service so a freshly written `.env` takes effect.
124
124
  **Errors:** `500` if the `.env` file is missing, or the restart could not be issued.
125
125
 
126
126
  ### `GET /api/setup/agent-image`
127
- Whether the `openclaw-agent-base` image is present.
127
+ Whether a supported OpenClaw version of the agent image is downloaded.
128
128
 
129
129
  **Auth:** open before a password exists, then browser cookie or bearer token (`requireAuthIfConfigured`)
130
130
 
131
- **Response:** `{ "exists": true, "imageId": "sha256:..." }`, or `{ "exists": false }`
132
- when the image is absent **or Docker is unreachable** — the two are not distinguished.
131
+ **Response:** `{ "exists": true, "version": "2026.9.3" }` (the newest one present), or
132
+ `{ "exists": false, "version": null }` when none is **or Docker is unreachable** — the two
133
+ are not distinguished.
133
134
 
134
135
  ### `POST /api/setup/agent-image`
135
- Pull or build the agent base image, streaming progress.
136
+ Download the newest supported version the registry publishes, streaming progress as SSE
137
+ (`status`, `progress`, `step`, `log`, `complete`, `error`). Nothing is downloaded when
138
+ that version is already here.
136
139
 
137
140
  **Auth:** open before a password exists, then browser cookie or bearer token (`requireAuthIfConfigured`)
138
141
 
@@ -350,7 +353,7 @@ Remaining credit for one provider.
350
353
 
351
354
  **Query params:** `provider` — one of `deepseek`, `openrouter`, `glm`.
352
355
 
353
- **Errors:** `502` when the provider's API cannot be reached or rejects the call.
356
+ **Errors:** `502` when the provider's API cannot be reached or rejects the call. For `glm`, Z.AI's token-quota endpoint only serves Coding Plan subscriptions: a pay-as-you-go key gets `502` with `reason: "no-balance-api"` — Z.AI exposes no balance API for such accounts (verified 2026-09-16), and the Gateway hides the badge on this reason instead of offering a retry.
354
357
 
355
358
  ### `POST /api/gateway/provider/oauth`
356
359
  Run the OAuth device flow for a provider that supports it.
@@ -673,8 +676,8 @@ List running agent containers (Docker containers with `AGENT_ID` label) with ful
673
676
  "id": "abc123def456",
674
677
  "agentId": "prometheus",
675
678
  "name": "prometheus",
676
- "image": "openclaw-agent-base:latest",
677
- "imageTag": "latest",
679
+ "image": "openclaw-agent-base:2026.9.3",
680
+ "imageTag": "2026.9.3",
678
681
  "template": "prometheus",
679
682
  "status": "running",
680
683
  "state": "running",
@@ -682,15 +685,20 @@ List running agent containers (Docker containers with `AGENT_ID` label) with ful
682
685
  "ip": "172.19.0.5",
683
686
  "created": "2026-07-01T16:58:42.876829416Z",
684
687
  "env": ["AGENT_ID=prometheus", "AGENT_TEMPLATE=prometheus"],
685
- "authToken": "***",
686
- "controlPort": "3033"
688
+ "controlPort": "3033",
689
+ "openclawVersion": "2026.9.3",
690
+ "updateAvailable": false
687
691
  }
688
692
  ]
689
693
  ```
690
694
 
691
695
  **Notes:**
692
- - The URL is always `http://<host-ip>:<port>#token=...` (every agent gets a host port mapping)
693
- - The `authToken` uses the shared gateway token from `data/agents-token.json`
696
+ - `openclawVersion` comes from the image's `org.opencontainers.image.version` label or
697
+ version tag, else from `openclaw --version` inside the running container (cached per
698
+ image id); `null` for a stopped agent whose image carries neither.
699
+ - `updateAvailable` is `true` when a newer supported OpenClaw version is downloaded.
700
+ - `controlPort` is the host port published for the Control UI. No link or token is
701
+ returned: the Open buttons go through `GET /api/agents/[id]/open-control-ui`
694
702
  - Containers are discovered via Docker API with label filter `AGENT_ID`
695
703
  - Template is inferred from the container image name + agent ID directory match in `agent-templates/`
696
704
 
@@ -785,7 +793,7 @@ Create a new agent container from a template.
785
793
 
786
794
  | Field | Required | Description |
787
795
  |---|---|---|
788
- | `name` | yes | Container name, also becomes `AGENT_ID` and subdomain |
796
+ | `name` | yes | Display name (`AGENT_NAME`); the container name and `AGENT_ID` are generated (`agent_<hex>`) |
789
797
  | `template` | yes | Template name (directory in `agent-templates/`) |
790
798
  | `portRange` | no | Optional port range (e.g. `3700-3709`) or single port (e.g. `3700`). Default: auto-assigned 10-port block |
791
799
  | `model` | no | Primary model id. Must be offered by this deployment (in the catalogue, enabled, provider has a key), in any form the proxy accepts; otherwise `400`. Defaults to `rev4a/deepseek/deepseek-flash`, validated the same way |
@@ -794,16 +802,18 @@ Create a new agent container from a template.
794
802
  **Every agent is created with a host port mapping:**
795
803
  - Port block: `-p <start>-<end>:3000-<3000+offset>` — maps a range of host ports to the same range starting at container port 3000
796
804
  - Single port: `-p <port>:3000` — maps a single host port to container port 3000
797
- - Control UI URL: composed by the browser as `http://<window.location.hostname>:<port>#token=<controlToken>` — the server returns the parts, not the URL
805
+ - Control UI: the wizard's Open button goes through `GET /api/agents/[id]/open-control-ui`, like the Agents page
798
806
  - Port 3740 is reserved for Rev4a
807
+ - Image: `openclaw-agent-base:<version>`, the newest supported OpenClaw version downloaded here; `409` when none is
799
808
 
800
809
  **Response:**
801
810
  ```json
802
811
  {
803
812
  "success": true,
804
813
  "containerId": "00f5de1eb08d...",
805
- "name": "my-agent",
806
- "image": "openclaw-agent-base:latest",
814
+ "containerName": "agent_2a3c3a07",
815
+ "displayName": "my-agent",
816
+ "image": "openclaw-agent-base:2026.9.3",
807
817
  "network": "rev4a-network",
808
818
  "controlToken": "asdfghjkl",
809
819
  "port": 3700,
@@ -856,6 +866,61 @@ docker command.
856
866
 
857
867
  ---
858
868
 
869
+ ### `PATCH /api/agents/[id]`
870
+ Edit an agent's display name, its port range, or both. The job runs in the background
871
+ (`lib/agent-edit.ts`); this answers `202` once it has started. The container is rebuilt
872
+ on the image it already runs — **without a backup**: the persistent volume is never
873
+ touched (`recreateAgentContainer()` in `lib/agent-recreate.ts`), then the gateway is
874
+ waited for (up to 5 minutes) and the runtime config re-applied.
875
+
876
+ The job is recorded in `agent_edits`, so a page reload or a Rev4a restart never loses it:
877
+ `GET` below reports the running or last edit, and while it runs anything else that would
878
+ touch the agent answers `409`.
879
+
880
+ **Auth:** browser cookie
881
+
882
+ **Body:**
883
+ ```json
884
+ { "displayName": "Argus", "portRange": "3700-3709" }
885
+ ```
886
+
887
+ `portRange` is `"<start>"` or `"<start>-<end>"`, mapped onto the gateway port 3000
888
+ (`3700-3709` → `3700-3709:3000-3009`). It is validated with the same rules and messages as
889
+ agent creation (`lib/agent-ports.ts`): a range that holds Rev4a's port `3740`, an invalid
890
+ format, or a port already published by **another** agent answers `400`
891
+ (`Ports already in use: 3711, 3712`) before anything is rebuilt. The agent's own published
892
+ ports are excluded, so keeping or shifting its block is not a conflict with itself.
893
+ At least one field is required; an empty `displayName` is rejected.
894
+
895
+ **Response (202):**
896
+ ```json
897
+ { "started": true, "id": 4 }
898
+ ```
899
+
900
+ **Errors:** `400` invalid body, invalid port range or empty display name; `409` an edit,
901
+ recreate, restore, update or backup of this agent is already running.
902
+
903
+ ---
904
+
905
+ ### `GET /api/agents/[id]`
906
+ The running or last edit of an agent (`null` when it was never edited) — what the edit
907
+ banner polls to resume after a reload. `status`: `rebuilding` → `done` | `failed`;
908
+ `interrupted` after a Rev4a restart cut it off.
909
+
910
+ **Auth:** browser cookie or bearer token
911
+
912
+ ```json
913
+ {
914
+ "edit": {
915
+ "id": 4, "agentId": "agent_2a3c3a07", "status": "rebuilding",
916
+ "displayName": "test 2 async", "portRange": null,
917
+ "error": null, "startedAtMs": 1790007512520, "finishedAtMs": null
918
+ }
919
+ }
920
+ ```
921
+
922
+ ---
923
+
859
924
  ### `POST /api/agents/[id]/restart`
860
925
  Restart an agent container (shorter than recreate — keeps everything intact).
861
926
 
@@ -870,105 +935,265 @@ Restart an agent container (shorter than recreate — keeps everything intact).
870
935
 
871
936
  ---
872
937
 
873
- ### `POST /api/agents/[id]/backup`
874
- Create a backup of the agent's persistent volume.
938
+ ### `GET /api/agents/[id]/backup`
939
+ List the agent's backup archives in Docker volume `rev4a-backups`.
875
940
 
876
- **Auth:** browser cookie
941
+ **Auth:** browser cookie or bearer token
877
942
 
878
943
  **Response:**
879
944
  ```json
880
- { "success": true, "file": "agent-prometheus-2026-07-12_043512345.tar.gz" }
945
+ { "backups": [ { "name": "agent-prometheus-2026-07-12_043512345.tar.gz", "size": "227.4 MB", "sizeBytes": 238442455, "date": "12/07/2026 04:35", "epoch": 1752292512 } ] }
881
946
  ```
882
947
 
883
- The backup file lives in Docker volume `rev4a-backups`. Backups exclude the npm
884
- package cache (`~/.npm/_cacache/`) to keep them small (~1.5 MB instead of 227 MB).
948
+ Archives are created by the cold backup (`POST /api/agents/[id]/cold-backup`) and
949
+ excluded the npm package cache (`~/.npm/_cacache/`). Sorted most-recent-first.
885
950
 
886
- **Error (404):** `{ "error": "No persistent volume found for agent '...'" }`
887
951
 
888
952
  ---
889
953
 
890
- ### `GET /api/agents/[id]/backup`
891
- List available backups for a specific agent.
954
+ ### `DELETE /api/agents/[id]/backup?file=...`
955
+ Remove one backup archive from `rev4a-backups`.
892
956
 
893
957
  **Auth:** browser cookie
894
958
 
895
- **Response:**
959
+ **Response:** `{ "success": true }`
960
+
961
+ **Error (400):** invalid or path-traversing file name.
962
+
963
+ ### `POST /api/agents/[id]/cold-backup`
964
+ Cold backup of the agent's volume (`lib/cold-backup.ts`). Returns `202` once the job
965
+ has started; the work continues in a helper container, `rev4a-backup-<id>`.
966
+
967
+ 1. The agent is stopped (`docker stop -t 30`, kill as a fallback) if it runs.
968
+ 2. The helper, started from the agent's own image, measures the volume (`du`),
969
+ refuses when `rev4a-backups` has less free space than about 60% of it, and writes
970
+ `agent-<id>-cold-<ts>.tar.gz.partial` (npm cache excluded) with GNU tar checkpoints
971
+ for progress.
972
+ 3. `tar -tzf` reads the whole archive back and must find `.openclaw/openclaw.json`;
973
+ only then is it renamed to `.tar.gz`. A failed or cancelled job leaves no archive.
974
+ 4. The agent is started again if it was running.
975
+
976
+ The job's state is the helper container and its labels, so it survives a Rev4a
977
+ restart: at startup, `reconcileColdBackups()` finishes helpers that exited and keeps
978
+ watching the rest.
979
+
980
+ While it runs, `POST restore`, `POST recreate`, `POST lifecycle`,
981
+ `PATCH` and `DELETE /api/agents/[id]` answer `409`, and the provider sync skips the
982
+ agent, reporting it in `sync.failed`.
983
+
984
+ **Auth:** browser cookie or bearer token
985
+
986
+ **Response (202):** `{ "started": true, "file": "agent-agent_9253eee3-cold-2026-09-16_000607123.tar.gz" }`
987
+
988
+ **Errors:** `404` no container or no volume; `409` a backup of this agent is already
989
+ running; `500` the helper could not start (the agent is started again if it was running).
990
+
991
+ ### `GET /api/agents/[id]/cold-backup`
992
+ The running or last job; `{ "backup": null }` when there is none since Rev4a started.
993
+
896
994
  ```json
897
995
  {
898
- "backups": [
899
- { "name": "agent-prometheus-2026-07-12_043512345.tar.gz", "size": "1.6 MB", "date": "12/07/2026 04:35" }
900
- ]
996
+ "backup": {
997
+ "agentId": "agent_9253eee3", "status": "running", "kind": "manual",
998
+ "file": "agent-agent_9253eee3-cold-2026-09-16_000607123.tar.gz",
999
+ "startedAtMs": 1789524367123, "finishedAtMs": null,
1000
+ "totalBytes": 2023456789, "doneBytes": 812400000, "percent": 40,
1001
+ "archiveBytes": null, "error": null
1002
+ }
901
1003
  }
902
1004
  ```
903
1005
 
904
- Backups are sorted most-recent-first.
1006
+ `status` is `running`, `succeeded` or `failed`; `error` is `cancelled` for a cancelled job.
1007
+
1008
+ ### `DELETE /api/agents/[id]/cold-backup`
1009
+ Cancel the running job: the helper is removed, the partial archive deleted and the
1010
+ agent started again if it was running. `409` when nothing is running.
905
1011
 
906
1012
  ---
907
1013
 
908
- ### `DELETE /api/agents/[id]/backup?file=agent-prometheus-2026-07-12_043512345.tar.gz`
909
- Remove a specific backup file.
1014
+ ### `POST /api/agents/[id]/restore?file=agent-prometheus-2026-07-12_043512345.tar.gz`
1015
+ Restore an agent's persistent volume from a backup. The job runs in the background
1016
+ (`lib/agent-restore.ts`); this answers `202` once it has started. No pre-restore backup is
1017
+ taken: restoring replaces the volume with the archive on purpose.
1018
+
1019
+ 1. The container is stopped if it runs.
1020
+ 2. The volume content is cleared and the archive extracted — up to 30 minutes, since a
1021
+ large workspace takes minutes to decompress. The file name reaches the container
1022
+ through its environment, never as shell syntax.
1023
+ 3. The container is started again even when the extract failed, so the agent never stays
1024
+ down. A volume with no container (volume-only agent) is restored anyway.
1025
+
1026
+ The job is recorded in `agent_restores`, so a page reload or a Rev4a restart never loses
1027
+ it: while it runs, anything else that would touch the agent answers `409`
1028
+ (`lib/agent-busy.ts`), and after a Rev4a restart an active row becomes `interrupted` with
1029
+ the container started again.
910
1030
 
911
- **Auth:** browser cookie
1031
+ **Auth:** browser cookie or bearer token
912
1032
 
913
- **Response:**
1033
+ **Response (202):**
914
1034
  ```json
915
- { "success": true }
1035
+ { "started": true, "id": 3 }
916
1036
  ```
917
1037
 
918
- **Error (400):** if filename is invalid or contains path traversal
1038
+ **Errors:** `400` invalid file name (not `agent-<id>-<safe charset>.tar.gz`); `404` the
1039
+ archive does not exist; `409` a restore, recreate, update, edit or backup of this agent
1040
+ is already running.
919
1041
 
920
1042
  ---
921
1043
 
922
- ### `POST /api/agents/[id]/restore?file=agent-prometheus-2026-07-12_043512345.tar.gz`
923
- Restore an agent's persistent volume from a backup.
1044
+ ### `GET /api/agents/[id]/restore`
1045
+ The running or last restore of an agent (`null` when it was never restored), with the
1046
+ archive being applied. `status`: `restoring` → `done` | `failed`; `interrupted` after a
1047
+ Rev4a restart cut it off.
924
1048
 
925
- The container is stopped, the volume content is replaced with the backup, then
926
- the container is restarted. If no container exists with the given `AGENT_ID`,
927
- the volume is restored anyway (useful for volume-only agents).
1049
+ **Auth:** browser cookie or bearer token
928
1050
 
929
- **Auth:** browser cookie
1051
+ ```json
1052
+ {
1053
+ "restore": {
1054
+ "id": 3, "agentId": "agent_2a3c3a07", "status": "restoring",
1055
+ "file": "agent-agent_2a3c3a07-cold-2026-09-21_172032317.tar.gz",
1056
+ "error": null, "startedAtMs": 1790004046548, "finishedAtMs": null
1057
+ }
1058
+ }
1059
+ ```
930
1060
 
931
- **Response:**
1061
+ ---
1062
+
1063
+ ### `POST /api/agents/[id]/recreate`
1064
+ Rebuild the agent container on its **own** OpenClaw version while preserving the
1065
+ persistent volume (workspace files, credentials, state DB, config). A recreate never
1066
+ changes version: moving to a newer OpenClaw version migrates the data one way, so it
1067
+ is a separate update. The job runs in the background (`lib/agent-recreate.ts`); this
1068
+ answers `202` once it has started.
1069
+
1070
+ The image is `openclaw-agent-base:<version>` for the version the agent runs
1071
+ (`resolveRecreateImage()` in `lib/agent-images.ts`). When that tag is missing but the
1072
+ container's image is still here, the image is tagged; when it is gone, a supported
1073
+ version is pulled. An agent whose version cannot be read keeps its exact image id.
1074
+
1075
+ Flow (each step recorded in `agent_recreates`):
1076
+ 1. **Cold backup** `agent-<id>-prerecreate-<ts>.tar.gz` — the agent stops for it, so
1077
+ the archive cannot catch its SQLite files mid-write. **If the backup fails, the
1078
+ recreate aborts and the agent is started again.**
1079
+ 2. The container is rebuilt (`lib/agent-recreate.ts`): same image tag, environment
1080
+ variables (`AGENT_*`, `MODEL_*`, `OPENCLAW_*`), labels, port mappings and network,
1081
+ reattaching the same volume. The network comes from the attached networks, or from
1082
+ the container's `NetworkMode` when its endpoint was lost.
1083
+ 3. The gateway is waited for (`/startupz`, up to 5 minutes), then `applyRuntimeConfig()`
1084
+ (`lib/agent-setup.ts`) guarantees the hooks, shared skills, provider proxy and
1085
+ Control UI origin policy.
1086
+ 4. On success, older `prerecreate` archives are pruned to the newest two per agent
1087
+ (manual `cold` and `preupdate` archives are never touched).
1088
+
1089
+ A failure before the rebuild starts the old container again, so a failed recreate never
1090
+ leaves the agent down. After a Rev4a restart, a job still active becomes `interrupted`
1091
+ at startup and the agent is started again — but only once its backup helper has
1092
+ finished, never while the archive is being written.
1093
+
1094
+ **Auth:** browser cookie or bearer token
1095
+
1096
+ **Response (202):**
932
1097
  ```json
933
- { "success": true, "container": "prometheus" }
1098
+ { "started": true, "id": 12 }
934
1099
  ```
935
1100
 
936
- **Error (404):** if the backup file is not found
1101
+ **Errors:** `404` when no image can be resolved; `409` when there is no container, an
1102
+ update, a recreate, a restore, an edit or a backup of this agent is running.
937
1103
 
938
1104
  ---
939
1105
 
940
- ### `POST /api/agents/[id]/recreate`
941
- Rebuild the agent container from the **latest** `openclaw-agent-base:latest` image
942
- while preserving the persistent volume (workspace files, credentials, state DB,
943
- config). This is how an existing agent picks up image/OpenClaw/CLI updates: the
944
- software lives in the image, the data in the volume.
945
-
946
- Flow:
947
- 1. **Auto-backup first.** The volume is backed up to `agent-<id>-prerecreate-<ts>.tar.gz`
948
- in `rev4a-backups`. **If the backup fails, the recreate is aborted** and the
949
- running container is left untouched.
950
- 2. The old container is removed (`docker rm -f`) and a new one is created with the
951
- same image tag, environment variables (`AGENT_*`, `MODEL_*`, `OPENCLAW_*`),
952
- labels, port mappings, and network — reattaching the same volume.
953
- 3. After startup, `applyRuntimeConfig()` (from `lib/agent-setup.ts`) guarantees:
954
- - `hooks.bootstrap-extra-files` (Rev4a system rules injection)
955
- - `skills.load.extraDirs` (shared skills discovery)
956
- - `models.providers.rev4a` (provider proxy, host-dependent)
957
- All patches use `config patch --stdin` (deep-merge) — existing user
958
- customizations are never overwritten.
959
-
960
- On the next boot, the image entrypoint runs `openclaw doctor --non-interactive`
961
- **only if the OpenClaw version changed** (safe migrations, no service restart),
962
- aligning the volume's state/config to the new binary.
1106
+ ### `GET /api/agents/[id]/recreate`
1107
+ The running or last recreate of an agent, with live backup progress (`null` when the
1108
+ agent was never recreated).
963
1109
 
964
- **Auth:** browser cookie
1110
+ **Auth:** browser cookie or bearer token
965
1111
 
966
- **Response:**
967
1112
  ```json
968
- { "success": true, "name": "prometheus", "image": "openclaw-agent-base:latest", "network": "openclaw-core_default", "backup": "agent-prometheus-prerecreate-2026-07-12_195512345.tar.gz" }
1113
+ {
1114
+ "recreate": {
1115
+ "id": 12, "agentId": "agent_9253eee3", "status": "backing_up",
1116
+ "image": "openclaw-agent-base:2026.7.1-2",
1117
+ "backupFile": "agent-agent_9253eee3-prerecreate-2026-09-17_171000.tar.gz",
1118
+ "backupPercent": 40, "error": null,
1119
+ "startedAtMs": 1789656372120, "finishedAtMs": null
1120
+ }
1121
+ }
969
1122
  ```
970
1123
 
971
- **Error (500):** `{ "error": "Pre-recreate backup failed, aborting: ..." }`
1124
+ - `status`: `backing_up` → `recreating` → `done` | `failed`; `interrupted` after a
1125
+ Rev4a restart cut it off.
1126
+ - `backupPercent` is live only while `backing_up`.
1127
+
1128
+ ---
1129
+
1130
+ ### `GET /api/agents/[id]/update`
1131
+ The agent's OpenClaw version, the update it could take, and its latest update
1132
+ (`lib/agent-update.ts`).
1133
+
1134
+ **Auth:** browser cookie or bearer token
1135
+
1136
+ ```json
1137
+ {
1138
+ "currentVersion": "2026.7.1-2",
1139
+ "candidate": { "fromVersion": "2026.7.1-2", "toVersion": "2026.9.3" },
1140
+ "update": {
1141
+ "id": 3, "agentId": "agent_9253eee3", "status": "backing_up",
1142
+ "fromVersion": "2026.7.1-2", "toVersion": "2026.9.3",
1143
+ "backupFile": "agent-agent_9253eee3-preupdate-2026.7.1-2-2026-09-16_003512345.tar.gz",
1144
+ "backupPercent": 40, "verification": null, "error": null,
1145
+ "startedAtMs": 1789524912000, "finishedAtMs": null
1146
+ }
1147
+ }
1148
+ ```
1149
+
1150
+ - `candidate` is `null` when no newer supported version is downloaded.
1151
+ - `update.status`: `pending` → `backing_up` → `migrating` → `verifying` → `done` |
1152
+ `failed`; `interrupted` after a Rev4a restart cut it off; `rolling_back` →
1153
+ `rolled_back` | `rollback_failed` for a rollback.
1154
+ - `verification`: `{ sessions, lostEvents: [{ session, before, after }], missingCronJobs, ok }`.
1155
+
1156
+ `503` when Docker cannot be reached.
1157
+
1158
+ ### `POST /api/agents/[id]/update`
1159
+ Start an update. **Body (optional):** `{ "version": "2026.9.3" }`; by default the newest
1160
+ supported version downloaded. Returns `202` with the new `update`; the steps continue in
1161
+ the background.
1162
+
1163
+ 1. **Preflight** (refused with `409`): the agent runs, the target is downloaded,
1164
+ supported and newer than the agent's version, no update or backup is running.
1165
+ 2. **Baseline**, inside the agent: transcript events per session (`.jsonl` lines on
1166
+ 2026.7.x, `transcript_events` rows from 9.x; trajectory files not counted) and cron
1167
+ job names.
1168
+ 3. **`backing_up`**: cold backup `agent-<id>-preupdate-<from>-<ts>.tar.gz`
1169
+ (`startColdBackup`, agent left stopped).
1170
+ 4. **`migrating`**: the container is recreated on `openclaw-agent-base:<to>`
1171
+ (`lib/agent-recreate.ts`); its entrypoint runs `doctor --fix`; Rev4a waits up to 10
1172
+ minutes for `/startupz` to report `started` with the target version, then re-applies
1173
+ its config and provider block.
1174
+ 5. **`verifying`**: every session has at least as many events as before and every cron
1175
+ job is still there → `done`, then unused agent images are removed (`pruneAgentImages`).
1176
+
1177
+ A failure before the recreate starts the old container again. A failure after it leaves
1178
+ the agent on the new version; the panel offers Rollback.
1179
+
1180
+ **Errors:** `400` unsupported version, `409` refused (reason in `error`), `500`.
1181
+
1182
+ While an update runs, `backup`, `cold-backup`, `restore`, `recreate`, `lifecycle`,
1183
+ `PATCH` and `DELETE /api/agents/[id]` answer `409 "An update of this agent is running"`,
1184
+ and the provider sync skips the agent.
1185
+
1186
+ ### `POST /api/agents/[id]/update/rollback`
1187
+ Roll back the latest update — `done`, `failed`, `interrupted` or `rollback_failed` — while
1188
+ its pre-update backup exists. `202` with the `update` (`rolling_back`).
1189
+
1190
+ 1. The previous version's image is made local (pulled when it is a supported version).
1191
+ 2. The agent is stopped and its volume replaced by the pre-update backup.
1192
+ 3. The container is recreated on `openclaw-agent-base:<from>` and must report that version.
1193
+
1194
+ Rev4a's config is not re-applied: the restored `openclaw.json` is the one that version
1195
+ accepted. Everything the agent did after the backup is lost. `409` when there is nothing
1196
+ to roll back, the backup is gone, or an update or backup is running.
972
1197
 
973
1198
  ---
974
1199
 
@@ -1057,7 +1282,7 @@ Disconnect Telegram from an agent.
1057
1282
  **Side effects:**
1058
1283
  - Sets `channels.telegram.enabled: false`, removes `botToken` and `allowFrom`
1059
1284
  - Removes the telegram binding
1060
- - Deletes pairing files (`telegram-pairing.json`, `telegram-default-allowFrom.json`)
1285
+ - Clears the channel's approved senders in OpenClaw's pairing store (`clearChannelAllowlist()`)
1061
1286
  - No restart required
1062
1287
 
1063
1288
  ---
@@ -1065,6 +1290,12 @@ Disconnect Telegram from an agent.
1065
1290
  ### `GET /api/agents/[id]/channels/pairing?channel=telegram`
1066
1291
  Get pending and approved pairings for a channel.
1067
1292
 
1293
+ Pending requests come from `openclaw pairing list --channel <channel> --json`. Approved
1294
+ senders come from OpenClaw's pairing store — on 2026.9.x the SQLite
1295
+ `channel_pairing_allow_entries` rows in `~/.openclaw/state/openclaw.sqlite`, read through
1296
+ the store's own `readChannelAllowFromStoreSync` (the credentials JSON files older
1297
+ releases used are gone, and reading those returned nothing).
1298
+
1068
1299
  **Auth:** browser cookie
1069
1300
 
1070
1301
  **Query params:**
@@ -1085,7 +1316,6 @@ Get pending and approved pairings for a channel.
1085
1316
  ```
1086
1317
 
1087
1318
  - Pending codes are from `openclaw pairing list --json`
1088
- - Approved senders are from the credentials allowFrom file
1089
1319
 
1090
1320
  ---
1091
1321
 
@@ -1115,6 +1345,15 @@ Approve a pending pairing code.
1115
1345
  ### `DELETE /api/agents/[id]/channels/pairing?channel=telegram&senderId=123456789`
1116
1346
  Revoke an approved sender.
1117
1347
 
1348
+ There is no CLI or RPC for this (`openclaw pairing` covers pending requests only,
1349
+ `channels.pairing.*` has no remove — verified on 2026.9.3). The pairing store's own
1350
+ writer is used instead: `removeChannelAllowFromStoreEntry`, located by function name in
1351
+ OpenClaw's `pairing-store` module and run inside the container, so the write goes
1352
+ through the same state transaction the CLI uses. Verifying a 2026.7.x agent is out of
1353
+ scope: Rev4a targets 2026.9.3. On a release that no longer exposes the writer the call
1354
+ fails with a message pointing at `/allowlist remove` from the chat — nothing is silently
1355
+ left unchanged.
1356
+
1118
1357
  **Auth:** browser cookie
1119
1358
 
1120
1359
  **Query params:**
@@ -1126,6 +1365,207 @@ Revoke an approved sender.
1126
1365
  **Response (200):** `{ "success": true }`
1127
1366
 
1128
1367
  **Response (400):** `{ "error": "senderId query param is required" }`
1368
+ **Response (404):** `{ "error": "..." }` when the store is unavailable
1369
+ (the sender is already gone when present — the desired state).
1370
+
1371
+ ---
1372
+
1373
+ ### `GET /api/agents/[id]/devices`
1374
+ Browser access to one agent's Control UI: requests waiting for approval and browsers
1375
+ already approved.
1376
+
1377
+ **Auth:** browser cookie
1378
+
1379
+ Runs `openclaw devices list --json` inside the container with the async `dockerExec`;
1380
+ see `lib/agent-devices.ts`. On OpenClaw 2026.7.x, which answers `/startupz` with HTML
1381
+ and whose config disables device approval, the CLI is not called and both lists are
1382
+ empty.
1383
+
1384
+ **Response:**
1385
+ ```json
1386
+ {
1387
+ "running": true,
1388
+ "requiresApproval": true,
1389
+ "pending": [
1390
+ {
1391
+ "requestId": "facd76cb-0dd5-462e-8d1d-d18ed35f6129",
1392
+ "deviceId": "f1ad26df8bc4…",
1393
+ "displayName": null,
1394
+ "platform": "MacIntel",
1395
+ "clientId": "openclaw-control-ui",
1396
+ "clientMode": "webchat",
1397
+ "roles": ["operator"],
1398
+ "scopes": ["operator.admin", "operator.read", "operator.write"],
1399
+ "remoteIp": "192.168.65.1",
1400
+ "browserOrigin": "http://192.168.1.88:3710",
1401
+ "isRepair": false,
1402
+ "requestedAtMs": 1789490956623
1403
+ }
1404
+ ],
1405
+ "approved": [
1406
+ {
1407
+ "deviceId": "848f99c9b0af…",
1408
+ "label": null,
1409
+ "platform": "MacIntel",
1410
+ "clientId": "openclaw-control-ui",
1411
+ "clientMode": "webchat",
1412
+ "roles": ["operator"],
1413
+ "scopes": ["operator.admin", "operator.read", "operator.write"],
1414
+ "remoteIp": "192.168.65.1",
1415
+ "approvedVia": "bootstrap",
1416
+ "createdAtMs": 1789492279000,
1417
+ "approvedAtMs": 1789492279000,
1418
+ "lastUsedAtMs": 1789492300000
1419
+ }
1420
+ ]
1421
+ }
1422
+ ```
1423
+
1424
+ - A stopped agent answers `200` with `running: false` and empty lists.
1425
+ - `requiresApproval` is `null` when neither `/startupz` nor the config can be read.
1426
+ - `approvedVia`: `silent` (local connection, approved automatically), `owner` (approved
1427
+ by an operator), `bootstrap` (one-time link), or another OpenClaw value.
1428
+ - Public keys and token values are never returned.
1429
+
1430
+ **Errors:** `404` unknown agent, `503` Docker unreachable, `502` the OpenClaw command
1431
+ failed or returned incomplete JSON.
1432
+
1433
+ ---
1434
+
1435
+ ### `POST /api/agents/[id]/devices`
1436
+ Approve or reject a pending request.
1437
+
1438
+ **Auth:** browser cookie
1439
+
1440
+ **Body:** `{ "action": "approve" | "reject", "requestId": "<requestId>" }`
1441
+
1442
+ **Response (200):** `{ "ok": true }`
1443
+
1444
+ **Errors:** `400` invalid action or `requestId`, `404` unknown agent, `409` agent not
1445
+ running, `502` command failed.
1446
+
1447
+ ---
1448
+
1449
+ ### `PATCH /api/agents/[id]/devices`
1450
+ Rename an approved browser. The label is preferred over the name the client reports.
1451
+
1452
+ **Auth:** browser cookie
1453
+
1454
+ **Body:** `{ "deviceId": "<deviceId>", "name": "Office laptop" }` — 1-64 printable characters
1455
+
1456
+ **Response (200):** `{ "ok": true }`
1457
+
1458
+ **Errors:** as for `POST`.
1459
+
1460
+ ---
1461
+
1462
+ ### `DELETE /api/agents/[id]/devices?deviceId=<deviceId>`
1463
+ Revoke an approved browser (`openclaw devices remove`). It needs approval again the next
1464
+ time it connects.
1465
+
1466
+ **Auth:** browser cookie
1467
+
1468
+ **Response (200):** `{ "ok": true }`
1469
+
1470
+ **Errors:** as for `POST`.
1471
+
1472
+ ---
1473
+
1474
+ ### `GET /api/agents/[id]/open-control-ui`
1475
+ Where the "Open" buttons navigate, in a new tab. Redirects (`302`) to the agent's
1476
+ Control UI.
1477
+
1478
+ **Auth:** browser cookie
1479
+
1480
+ The target host is the one the request reached Rev4a on (the `Host` header), with the
1481
+ agent's published port — never a parameter, so the link cannot be sent to another host.
1482
+
1483
+ - **OpenClaw 9.x** (it answers `/startupz` with JSON): runs `openclaw dashboard --json`
1484
+ in the container and redirects to its `browserUrl`, with the host, the port and the
1485
+ `gatewayUrl` fragment parameter rewritten to that host and the published port. The
1486
+ link is single-use, expires after ten minutes, and pairs the browser with no approval.
1487
+ - **OpenClaw 2026.7.x**, or when no one-time link can be issued: redirects to the plain
1488
+ link `http://<host>:<port>/#token=<token>`, with the token read inside the container
1489
+ (`/root/.agent-token`, then `OPENCLAW_GATEWAY_TOKEN`).
1490
+
1491
+ A navigation, not a JSON call: the button opens this URL inside the click, so no tab
1492
+ is left blank after an `await` and no popup can be blocked.
1493
+
1494
+ **Errors** (plain text, meant for the tab): `400` host not readable, `404` unknown agent,
1495
+ `409` agent not running or no published Control UI port, `503` Docker unreachable,
1496
+ `502` neither a one-time link nor a token available.
1497
+
1498
+ ---
1499
+
1500
+ ### `GET /api/agents/[id]/invite-link`
1501
+ A Control UI link to send to someone else — the "Invite link" button in Browser access.
1502
+ The browser that opens it passes gateway auth and waits in the agent's approval list;
1503
+ nothing opens until an operator approves it.
1504
+
1505
+ **Auth:** browser cookie
1506
+
1507
+ **Response:**
1508
+ ```json
1509
+ { "url": "http://212.0.113.7:3710/#token=…", "loopbackHost": false }
1510
+ ```
1511
+
1512
+ - `url` is the plain token link. The host is the one the request reached Rev4a on
1513
+ (the `Host` header); the port is the agent's published Control UI port. The token is
1514
+ read inside the container with the entrypoint's precedence (`/root/.agent-token`,
1515
+ then `OPENCLAW_GATEWAY_TOKEN`), so it is the one the Gateway checks.
1516
+ - `loopbackHost` is true when that host is `localhost`, `127.x` or `[::1]`: nobody else
1517
+ can reach the link.
1518
+ - The gateway token is shared by every agent. Changing the agents token
1519
+ (`PUT /api/agents/token`) invalidates every link sent.
1520
+ - `Cache-Control: no-store`.
1521
+
1522
+ **Errors:** `400` host not readable, `404` unknown agent, `409` agent not running, no
1523
+ published Control UI port, or no browser approval on this agent (the link would open
1524
+ with no approval), `502` approval state or token not readable, `503` Docker
1525
+ unreachable.
1526
+
1527
+ ---
1528
+
1529
+ ### `GET /api/agents/devices-summary`
1530
+ Browsers waiting for approval, per running agent — the "browser waiting" badge on the
1531
+ agent list.
1532
+
1533
+ **Auth:** browser cookie
1534
+
1535
+ **Response:**
1536
+ ```json
1537
+ { "agents": [ { "agentId": "agent_2a3c3a07", "requiresApproval": true, "pending": 1 } ] }
1538
+ ```
1539
+
1540
+ - At most three agents are read at a time; one that cannot be read reports
1541
+ `error: true` and `pending: 0` without failing the others.
1542
+ - `503` with `agents: []` when Docker cannot be reached.
1543
+
1544
+ ---
1545
+
1546
+ ### `GET /api/agents/activity-summary`
1547
+ The long operation each agent is in the middle of, if any — the chip on the agent cards:
1548
+ `BACKUP nn%`, `RESTORING`, `RECREATING`, `UPDATING`.
1549
+
1550
+ **Auth:** browser cookie or bearer token
1551
+
1552
+ **Response:**
1553
+ ```json
1554
+ { "agents": [
1555
+ { "agentId": "agent_9253eee3", "kind": "backup", "file": "agent-agent_9253eee3-cold-2026-09-17_163103795.tar.gz", "percent": 16 },
1556
+ { "agentId": "agent_2a3c3a07", "kind": "restore", "file": "agent-agent_2a3c3a07-cold-2026-09-21_172032317.tar.gz" },
1557
+ { "agentId": "agent_fb540be6", "kind": "update", "version": "2026.9.3" }
1558
+ ] }
1559
+ ```
1560
+
1561
+ - `kind`: `backup`, `restore`, `recreate`, `update` or `edit`. A running cold backup takes
1562
+ precedence, because while its helper archives the volume that is the phase actually in
1563
+ progress (an update's or recreate's own backup phase reports `backup`).
1564
+ - `percent` is only on `backup` (`null` until the archive size has been measured; the
1565
+ client renders `BACKUP…` then). `version` is on `recreate`/`update`.
1566
+ - Idle agents are not listed: an idle system answers `{ "agents": [] }`.
1567
+ - Read from the state modules (`agent_restores`, `agent_recreates`, `agent_upgrades`,
1568
+ `agent_edits`) and one Docker list call for the running backup helpers.
1129
1569
 
1130
1570
  ---
1131
1571
 
@@ -1159,7 +1599,7 @@ Useful for inspecting the container config without SSH or terminal.
1159
1599
  {
1160
1600
  "name": "my-agent",
1161
1601
  "config": {
1162
- "gateway": { "controlUi": { "allowedOrigins": ["..."], "dangerouslyDisableDeviceAuth": true } },
1602
+ "gateway": { "controlUi": { "dangerouslyAllowHostHeaderOriginFallback": true } },
1163
1603
  "agents": { "defaults": { "model": { "primary": "rev4a/deepseek/deepseek-flash", "fallbacks": [] } } },
1164
1604
  "models": { "providers": { "rev4a": { "baseUrl": "...", "apiKey": "***", "models": [...] } } }
1165
1605
  }
@@ -1187,6 +1627,9 @@ The token is stored in `data/agents-token.json`.
1187
1627
 
1188
1628
  ### `PUT /api/agents/token`
1189
1629
  Update the shared agents gateway token and push it to all running agent containers.
1630
+ Agents with a long operation in flight (update, recreate, restore, edit, backup) are
1631
+ skipped and listed, and a container that could not be updated is reported instead of
1632
+ being swallowed.
1190
1633
 
1191
1634
  **Auth:** browser cookie
1192
1635
 
@@ -1197,7 +1640,7 @@ Update the shared agents gateway token and push it to all running agent containe
1197
1640
 
1198
1641
  **Response:**
1199
1642
  ```json
1200
- { "success": true, "containersUpdated": 2 }
1643
+ { "success": true, "containersUpdated": 2, "skipped": ["agent_9253eee3"], "failed": [{ "container": "agent_x", "error": "…" }] }
1201
1644
  ```
1202
1645
 
1203
1646
  **Side effects:**
@@ -1210,53 +1653,64 @@ Update the shared agents gateway token and push it to all running agent containe
1210
1653
  ## Agent Image Management
1211
1654
 
1212
1655
  ### `GET /api/agents/image-status`
1213
- Check the status of the `openclaw-agent-base` Docker image.
1656
+ Which OpenClaw versions of the agent image are downloaded, and whether the registry
1657
+ publishes a newer supported one.
1214
1658
 
1215
1659
  **Auth:** browser cookie or bearer token
1216
1660
 
1217
- The ghcr.io digest lookup is cached for 10 minutes and de-duplicated while in
1218
- flight, so polling this endpoint does not re-query the registry every time.
1661
+ The registry tag list is read over the registry HTTP API (anonymous token for ghcr,
1662
+ plain HTTP for a `localhost` registry), cached for 10 minutes and de-duplicated while
1663
+ in flight, so polling this endpoint does not re-query the registry every time.
1219
1664
 
1220
1665
  **Response:**
1221
1666
  ```json
1222
1667
  {
1668
+ "downloading": false,
1669
+ "downloadingVersion": null,
1223
1670
  "exists": true,
1224
1671
  "needsUpdate": false,
1225
- "downloading": false,
1226
- "localId": "sha256:a1b2c3d4e5f6..."
1672
+ "localVersions": ["2026.9.3"],
1673
+ "newestLocal": "2026.9.3",
1674
+ "available": null,
1675
+ "registryReachable": true
1227
1676
  }
1228
1677
  ```
1229
1678
 
1230
1679
  | Field | Description |
1231
1680
  |---|---|
1232
- | `exists` | Whether `openclaw-agent-base:latest` exists locally |
1233
- | `needsUpdate` | `true` if local manifest digest differs from remote registry |
1234
- | `downloading` | Whether a download is currently running |
1235
- | `localId` | SHA256 ID of the local image, or null |
1681
+ | `exists` | A supported version is downloaded as `openclaw-agent-base:<version>` |
1682
+ | `needsUpdate` | `true` when none is, or when `available` is set |
1683
+ | `localVersions` | Supported versions downloaded, newest first |
1684
+ | `newestLocal` | The version new agents are created on |
1685
+ | `available` | Newest supported version on the registry that is not downloaded and is newer than `newestLocal`, or null |
1686
+ | `downloading` / `downloadingVersion` | A download running, and its version |
1687
+ | `registryReachable` | The tag list could be read |
1236
1688
 
1237
1689
  ### `POST /api/agents/download-image`
1238
- Starts pulling or building the `openclaw-agent-base:latest` image. Returns 202
1239
- Accepted immediately — the operation runs in the background and writes logs.
1240
- The frontend polls `GET /api/agents/image-status` for progress updates.
1690
+ Downloads one OpenClaw version of the agent image in the background. Returns 202
1691
+ immediately; the frontend polls `GET /api/agents/image-status` for completion.
1241
1692
 
1242
1693
  **Auth:** browser cookie or bearer token
1243
1694
 
1695
+ **Body (optional):** `{ "version": "2026.9.3" }` — a supported version. Without it, the
1696
+ newest supported version the registry publishes.
1697
+
1244
1698
  **Response (202):**
1245
1699
  ```json
1246
- { "status": "downloading" }
1700
+ { "started": true, "version": "2026.9.3" }
1247
1701
  ```
1248
1702
 
1249
- **Error (409):**
1250
- ```json
1251
- { "error": "A download is already in progress" }
1252
- ```
1703
+ **Errors:** `400` unsupported version; `409` a download is already in progress.
1253
1704
 
1254
- Image state is managed in `lib/buildAgentImage.ts` (library functions, not HTTP routes):
1255
- - `downloadAgentImage(opts)` — attempts `docker pull` from registry first, then falls back to `docker build`
1256
- all stdout/stderr to a log file, supports optional `onEvent` callback and `AbortSignal`
1257
- - `abortDownload()` — sends SIGKILL to the child process, releases the download lock
1258
- - `getDownloadLogPath()` — returns the path to the current/last download log file
1259
- - `getIsDownloading()` — checks the in-memory lock flag
1705
+ The download is `lib/buildAgentImage.ts` → `downloadAgentImage({ version, onEvent, signal })`:
1706
+ - nothing to do when `openclaw-agent-base:<version>` is already here
1707
+ - `docker pull <registry>:<version>`, tag `openclaw-agent-base:<version>`, untag the
1708
+ registry reference (`pullAgentImage()` in `lib/agent-images.ts`)
1709
+ - when the pull fails and the repository Dockerfile's `ARG OPENCLAW_VERSION` is that
1710
+ version, `docker build --build-arg OPENCLAW_VERSION=<version>` instead; any other
1711
+ version fails
1712
+ - one download at a time (in-process lock, `getIsDownloading()` /
1713
+ `getDownloadingVersion()`); output goes to `/tmp/rev4a-download-<timestamp>.log`
1260
1714
 
1261
1715
  ### `GET /api/agents-active`
1262
1716
 
@@ -1609,7 +2063,7 @@ List all running Docker containers with resource usage.
1609
2063
  "containers": [
1610
2064
  {
1611
2065
  "name": "openclaw-atlas",
1612
- "image": "openclaw-agent-base:latest",
2066
+ "image": "openclaw-agent-base:2026.9.3",
1613
2067
  "status": "running",
1614
2068
  "ports": ["0.0.0.0:3731->3000/tcp"],
1615
2069
  "created": "2026-06-20T10:00:00Z",