diff --git a/.env.example b/.env.example index 5164854..9eeaf42 100644 --- a/.env.example +++ b/.env.example @@ -162,7 +162,9 @@ PIPELINE_VERBATIM_MAX_CHARS=200000 # reach the bot's own files or another server's. WORKSPACE_ENABLED=true # Base directory the per-server workspaces live under. Blank puts it at -# .workspace/ beside the bot; set an absolute path to mount a volume there. +# .workspace/ beside the bot. The Docker image sets this to /data/workspace, +# so a volume mounted at /data (a Railway volume, or `docker run -v`) keeps +# the workspace across redeploys. Set any absolute path to override. WORKSPACE_ROOT= # Caps: the largest single file, the whole workspace, and the file count. WORKSPACE_MAX_FILE_KB=64 diff --git a/Dockerfile b/Dockerfile index 8684fea..97ab2f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,4 +42,13 @@ ENV PLUGIN_HTTP_ENABLED=true \ PLUGIN_HTTP_MAX_REDIRECTS=3 \ PLUGIN_HTTP_ALLOW_PRIVATE=false +# Persistent storage. The agent file workspace lives under /data so it +# survives a redeploy: attach a Railway volume (or `docker run -v`) with the +# mount path /data and everything the files.* and shell.run tools write +# persists. With nothing mounted, /data is an ordinary directory and the +# workspace is ephemeral, exactly as before. An --env-file at run time still +# overrides WORKSPACE_ROOT. +RUN mkdir -p /data/workspace +ENV WORKSPACE_ROOT=/data/workspace + CMD ["python", "main.py"] diff --git a/README.md b/README.md index 69e0e18..f9c043a 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,16 @@ boot -- it is idempotent. ```sh docker build -t archimedes . -docker run --env-file .env archimedes +docker run --env-file .env -v archimedes-data:/data archimedes ``` -A `railway.toml` is included for one-click Railway deploys. +The agent file workspace lives under `/data` (`WORKSPACE_ROOT` is set to +`/data/workspace` in the image), so mounting a volume there keeps it across +restarts; without `-v` the workspace is ephemeral. + +A `railway.toml` is included for one-click Railway deploys. On Railway, +attach a volume to the service with the mount path `/data` to get the same +persistence. ## Configuration diff --git a/cogs/chat.py b/cogs/chat.py index ea2691a..f6d46d6 100644 --- a/cogs/chat.py +++ b/cogs/chat.py @@ -416,7 +416,7 @@ async def _stream_turn( async def _approver(name: str, args: dict) -> bool: return await self._collect_tool_approval( - channel_id, user_id, name, args) + placeholder.channel, user_id, name, args) tool_ctx.approver = _approver renderer = StreamRenderer(placeholder) @@ -454,18 +454,19 @@ async def _approver(name: str, args: dict) -> bool: return final_text or None async def _collect_tool_approval( - self, channel_id: int, user_id: int, name: str, args: dict, + self, channel, user_id: int, name: str, args: dict, ) -> bool: """Post an Approve / Reject prompt for one gated tool call. - Returns the human decision. A send failure or an unanswered prompt - counts as a refusal -- a gated tool is never run without an explicit - yes. The prompt message is edited in place with the outcome, so the - channel keeps a record of who cleared what. + ``channel`` is the channel the turn is replying in -- a guild channel, + a thread or a DM alike -- taken straight from the placeholder message, + so the prompt always lands where the user is looking and never depends + on a channel-cache lookup that misses for DMs. Returns the human + decision; a send failure or an unanswered prompt counts as a refusal, + so a gated tool is never run without an explicit yes. The prompt is + edited in place with the outcome, so the channel keeps a record of who + cleared what. """ - channel = self.bot.get_channel(channel_id) - if channel is None: - return False timeout = float(max(5, Config.AGENT_APPROVAL_TIMEOUT_S)) decision: asyncio.Future[bool] = ( asyncio.get_running_loop().create_future() diff --git a/railway.toml b/railway.toml index 8c54924..cdc9bc1 100644 --- a/railway.toml +++ b/railway.toml @@ -1,3 +1,9 @@ +# Persistent storage: attach a volume to this service in the Railway +# dashboard with the mount path /data. The agent file workspace then lives on +# it -- WORKSPACE_ROOT is set to /data/workspace in the Dockerfile -- and +# survives every redeploy. Railway volumes are attached in the dashboard, not +# declared in this file. + [build] builder = "DOCKERFILE" dockerfilePath = "Dockerfile"