diff --git a/.gitea/workflows/sync-upstream.yml b/.gitea/workflows/sync-upstream.yml index 72c93f15ac..7ef1c7cb80 100644 --- a/.gitea/workflows/sync-upstream.yml +++ b/.gitea/workflows/sync-upstream.yml @@ -215,6 +215,21 @@ jobs: DOCTOR_RC=0 docker exec "$SMOKE" dsh-workspace-doctor.mjs --check > /dev/null || DOCTOR_RC=$? [ "$DOCTOR_RC" -le 1 ] + # ...and the entrypoint must actually invoke it. The doctor prints a + # "dsh-workspace-doctor:" line when it runs; the entrypoint logs a + # "workspace doctor:" line only when it could not. A direct + # `docker exec` above sails past a wrong path or a lost exec bit, so + # the boot log, not the binary, is what proves the wiring. + if ! docker logs "$SMOKE" 2>&1 | grep -q "^dsh-workspace-doctor:"; then + echo "the workspace doctor did not run at boot" + docker logs "$SMOKE" | tail -30 + exit 1 + fi + if docker logs "$SMOKE" 2>&1 | grep -q "workspace doctor:"; then + echo "the entrypoint could not run the workspace doctor" + docker logs "$SMOKE" | tail -30 + exit 1 + fi OK="" for i in $(seq 1 30); do if docker exec "$SMOKE" node -e ' diff --git a/README.md b/README.md index 7bd0b300c7..598e8b1ff9 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,13 @@ Your DeepSeek API key can be set below (stored in the container env) or entered ``` +An **existing** container keeps the fields it was created with: Unraid does not add a +variable a template gained later, so the Workspace Doctor row above does nothing until you +add it by hand (Docker → Edit → *Add another Path, Port, Variable, Label or Device* → +Variable, key `DSH_WORKSPACE_DOCTOR`, value `repair`) or re-apply the template. Without it +the container stays on the image default (`warn`): it reports lost grouping at every start +but never repairs it. + Behind a Cloudflare Tunnel, add the public hostname as a `--trusted-host` and have the tunnel rewrite `Host` to that name (and strip `Origin`) so the fence sees a name it trusts. @@ -330,7 +337,17 @@ it fixes it at startup: | Repairs | recreates missing workspace directories, and re-accounts chats whose recorded directory matches a project but which no project lists | | Never | touches a session log, deletes a session, invents a project, or moves a chat into a project it did not run in | | Before writing | copies the workspace registry to `workspace.json.bak-`; the write itself is atomic and in dsh's own format | -| By hand | `docker exec -u 1000:1000 dsh-workspace-doctor.mjs --check` (add `--all --dry-run` to preview a repair) | +| By hand | report with `docker exec dsh-workspace-doctor.mjs --check`; repair with the one-off container below | + +To repair without waiting for a container start, stop the container first so nothing races +the registry write, then run the doctor against the state volume directly (`--entrypoint` +bypasses dsh, and the image's `PUID`/`PGID` are applied to what it writes): + +```sh +docker run --rm --entrypoint /usr/local/bin/dsh-workspace-doctor.mjs \ + -v /mnt/user/appdata/deepseek-harness:/home/node/.dsh \ + gitea.milesward.dev/mward4/deepseek-harness:latest --all +``` The recreated directory is empty: this restores the *grouping*, not the files — those lived in the container and nothing here can bring them back. Keep project data under `/workspace` diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 1e7468b016..60e2b052e8 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -137,17 +137,33 @@ if [ "$(id -u)" = 0 ]; then # recreate the directories and re-account the orphaned sessions when asked # (backup first, session logs never touched). See README: # "Chats land in Ungrouped after an update". + # + # Resolve it by name: the installed file is dsh-workspace-doctor.mjs, and a + # path spelled out here is exactly how a "not found" reaches production. The + # CI smoke test asserts this step's output appears in the boot log, so a + # rename or a wrong spelling fails the build instead of the container start. + DOCTOR="$(command -v dsh-workspace-doctor.mjs 2>/dev/null || true)" case "${DSH_WORKSPACE_DOCTOR:-warn}" in 0|off|no) ;; repair|all|1|yes) - /usr/local/bin/dsh-workspace-doctor --all \ - || log "workspace doctor: repair did not finish; starting dsh with the registry unchanged" - # The doctor runs as root here; hand what it wrote back to the runtime user. - chown "$PUID:$PGID" "$STATE/storages/workspace.json" "$STATE/storages/"workspace.json.bak-* 2>/dev/null || true + if [ -n "$DOCTOR" ]; then + "$DOCTOR" --all \ + || log "workspace doctor: repair did not finish; starting dsh with the registry unchanged" + # The doctor runs as root here; hand what it wrote back to the runtime user. + chown "$PUID:$PGID" "$STATE/storages/workspace.json" "$STATE/storages/"workspace.json.bak-* 2>/dev/null || true + else + log "workspace doctor: dsh-workspace-doctor.mjs is not installed in this image;" + log " workspace grouping was NOT repaired. Update the image." + fi ;; *) - /usr/local/bin/dsh-workspace-doctor --check \ - || log "workspace doctor: see the report above - set DSH_WORKSPACE_DOCTOR=repair to fix it at startup" + if [ -n "$DOCTOR" ]; then + "$DOCTOR" --check \ + || log "workspace doctor: see the report above - set DSH_WORKSPACE_DOCTOR=repair to fix it at startup" + else + log "workspace doctor: dsh-workspace-doctor.mjs is not installed in this image;" + log " workspace grouping was NOT checked. Update the image." + fi ;; esac @@ -163,6 +179,9 @@ if [ ! -w "$STATE" ]; then log " chown -R $(id -u):$(id -g) " exit 1 fi -/usr/local/bin/dsh-workspace-doctor --check \ - || log "workspace doctor: see the report above (running unprivileged, so it only reports)" +DOCTOR="$(command -v dsh-workspace-doctor.mjs 2>/dev/null || true)" +if [ -n "$DOCTOR" ]; then + "$DOCTOR" --check \ + || log "workspace doctor: see the report above (running unprivileged, so it only reports)" +fi exec dsh "$@"