From 3849709cc2293582860490df1725fe65028abb75 Mon Sep 17 00:00:00 2001 From: dsh agent Date: Thu, 17 Sep 2026 22:21:58 -0400 Subject: [PATCH] Fix the doctor's invocation path; assert the boot log in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entrypoint called /usr/local/bin/dsh-workspace-doctor while the Dockerfile installs /usr/local/bin/dsh-workspace-doctor.mjs, so every start logged "...: not found" and the workspace doctor never ran: no report, no repair. The CI smoke test checked the .mjs path with a direct `docker exec`, which cannot see a mistake in the entrypoint's own invocation, so the build stayed green while the container was broken. Resolve the doctor by name (`command -v dsh-workspace-doctor.mjs`) in both the root and the non-root path, and log an explicit "not installed in this image" line when it cannot be found, instead of the shell's bare "not found". The smoke test now reads the container's boot log: it requires the doctor's own "dsh-workspace-doctor:" output to appear, and fails if the entrypoint logs its "workspace doctor:" failure line — that covers path, exec bit and invocation together, which is the class of mistake that shipped. README: manual repair through a one-off container with --entrypoint (dsh stopped, so nothing races the registry write), and the note that an existing Unraid container does not pick up the template's new Workspace Doctor variable on its own. --- .gitea/workflows/sync-upstream.yml | 15 +++++++++++++ README.md | 19 +++++++++++++++- docker-entrypoint.sh | 35 +++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 9 deletions(-) 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 "$@" -- 2.54.0