Persistent workers¶
An interactive terminal or chat does not stay alive forever. A persistent worker runs under the host's service manager, polls Griglia and starts a fresh non-interactive agent session whenever work is assigned. Closing the terminal, browser or original agent session does not stop it.
Griglia ships the worker and a systemd user-service template with its other host scripts:
The worker is vendor-neutral around the board contract: every instance uses its own agent key with
griglia:check --agent=<key>, its own lock and the same task states. Built-in launch drivers are available
for Codex CLI and Claude Code; a JSON argv template connects another CLI without shell evaluation.
Install the systemd user service¶
Copy the example and replace /absolute/path/to/project in both lines with the real absolute project path:
mkdir -p ~/.config/systemd/user
cp scripts/systemd/griglia-agent-worker@.service.example \
~/.config/systemd/user/griglia-agent-worker@.service
sed -i 's#/absolute/path/to/project#/srv/my-project#g' \
~/.config/systemd/user/griglia-agent-worker@.service
systemctl --user daemon-reload
Enable one instance per configured agent. The instance name is the Griglia agent key:
systemctl --user enable --now griglia-agent-worker@codex.service
systemctl --user enable --now griglia-agent-worker@claude.service
Multiple applications on the same computer¶
Run one worker per application and agent: each worker polls one board and starts the agent in that
project's directory. The lock automatically includes both repository and agent key, so two applications can
both use codex without blocking each other.
For each application, copy the template under a unique unit prefix and replace its project path. For example:
cp app-one/scripts/systemd/griglia-agent-worker@.service.example \
~/.config/systemd/user/griglia-agent-worker-app-one@.service
sed -i 's#/absolute/path/to/project#/srv/app-one#g' \
~/.config/systemd/user/griglia-agent-worker-app-one@.service
cp app-two/scripts/systemd/griglia-agent-worker@.service.example \
~/.config/systemd/user/griglia-agent-worker-app-two@.service
sed -i 's#/absolute/path/to/project#/srv/app-two#g' \
~/.config/systemd/user/griglia-agent-worker-app-two@.service
systemctl --user daemon-reload
systemctl --user enable --now griglia-agent-worker-app-one@codex.service
systemctl --user enable --now griglia-agent-worker-app-two@codex.service
Shared agent configuration remains in ~/.config/griglia-worker/codex.env. Project-specific settings can
override it in ~/.config/griglia-worker/griglia-agent-worker-app-one-codex.env (the unit's %p-%i.env
pattern). In particular, each Docker application needs a distinct GRIGLIA_WORKER_CONTAINER; for local
transport set GRIGLIA_WORKER_REPO and, when needed, GRIGLIA_WORKER_PHP instead.
codex invokes codex exec --approve-for-me; claude invokes
claude -p --permission-mode bypassPermissions. The unit adds %h/.local/bin to PATH, the usual location for
user-installed launchers. If command -v codex or command -v claude reports another directory, put a
complete PATH=... line in ~/.config/griglia-worker/<agent-key>.env.
Inspect the service and follow its output:
systemctl --user status griglia-agent-worker@codex.service
journalctl --user -u griglia-agent-worker@codex.service -f
To keep user services running after logout and start them during boot, enable lingering once:
Configuration¶
Each instance optionally reads ~/.config/griglia-worker/<agent-key>.env:
GRIGLIA_WORKER_DRIVER=codex
GRIGLIA_WORKER_INTERVAL=10
GRIGLIA_WORKER_RETRY_DELAY=30
GRIGLIA_WORKER_MAX_PARALLEL=2
GRIGLIA_WORKER_TRANSPORT=docker
GRIGLIA_WORKER_CONTAINER=laravel-dev-app
GRIGLIA_WORKER_REPO=/srv/my-project
The Docker transport is the default and runs docker exec <container> php artisan. If Laravel runs directly
on the worker host, use the local transport instead; Artisan runs with the repository as working directory, so
no Docker is involved anywhere in the loop:
GRIGLIA_WORKER_TRANSPORT=local
GRIGLIA_WORKER_PHP=/usr/bin/php8.4
GRIGLIA_WORKER_REPO=/srv/my-project
The GRIGLIA_WORKER_* names configure one instance. When they are absent the worker falls back to the
variables the other host scripts read — GRIGLIA_TRANSPORT, GRIGLIA_PHP, GRIGLIA_CONTAINER —
so a single choice, exported once for the machine, covers the worker and the helpers the agent itself runs
(token counting, context and skill synchronization). Every setting also has a flag, useful for a one-off run:
| Flag | Env variable | Default |
|---|---|---|
--transport docker\|local |
GRIGLIA_WORKER_TRANSPORT, GRIGLIA_TRANSPORT |
docker |
--container |
GRIGLIA_WORKER_CONTAINER, GRIGLIA_CONTAINER |
laravel-dev-app |
--php |
GRIGLIA_WORKER_PHP, GRIGLIA_PHP |
php |
--repo |
GRIGLIA_WORKER_REPO |
current directory |
--driver codex\|claude\|custom |
GRIGLIA_WORKER_DRIVER |
the agent key |
--interval, --retry-delay |
GRIGLIA_WORKER_INTERVAL, GRIGLIA_WORKER_RETRY_DELAY |
10, 30 |
--max-parallel |
GRIGLIA_WORKER_MAX_PARALLEL |
2 |
--model |
GRIGLIA_WORKER_MODEL |
the agent CLI's own default |
--effort |
GRIGLIA_WORKER_EFFORT |
the agent CLI's own default |
The driver defaults to the agent key, so keys named codex and claude need no env file. If the key is
different, set the matching driver explicitly.
Model and reasoning effort¶
Without further configuration each session uses the model the agent CLI is configured with. GRIGLIA_WORKER_MODEL
and GRIGLIA_WORKER_EFFORT choose them per worker instead, so the board agent can run on a different model than
the interactive sessions of the same CLI:
The claude driver passes them as --model and --effort (low, medium, high, xhigh, max); the
codex driver as --model and -c model_reasoning_effort="<effort>". The custom driver receives them as the
{model} and {effort} placeholders (empty strings when unset), so an argv template decides where they go.
Values are not validated by the worker: an unknown model or effort level fails inside the agent CLI.
The variables are read when the worker starts. To apply a change without interrupting the sessions that are running, drain the worker instead of restarting it — see Updating a running worker.
For Gemini CLI, Aider or another agent, use the custom driver. The JSON array is executed directly (never
through a shell); {prompt}, {repo}, {agent}, {model} and {effort} are replaced in individual arguments:
GRIGLIA_WORKER_DRIVER=custom
GRIGLIA_WORKER_COMMAND_JSON=["agent-cli","--cwd","{repo}","--prompt","{prompt}"]
Transport and driver are independent, so Codex, Claude and custom drivers work in both modes. The service account must be able to run Docker or the configured local PHP executable, and the selected agent CLI non-interactively. Do not use unrestricted sandbox/approval bypass flags: grant only the project permissions the workflow needs.
Behaviour and testing¶
The worker polls the current board state, so it also finds work that was already open before a restart. In
ordered mode it runs exactly one session. In multitasking mode it runs up to --max-parallel sessions
(default 2), one per eligible task; reduce the limit when tasks can touch the same files. One flock per
repository/agent pair prevents duplicate worker processes, while the worker tracks every child by task id.
Before launching a CLI for an open task, the worker takes it through griglia:check --take: this repeats the
board's current ownership check. If the user changed the task agent or its list default after the poll
snapshot, the board refuses the stale claim and the wrong agent is never started.
When the fresh snapshot produced by agent-status.py reports a usage window at 100%, the board exposes its
reset time to the worker. The worker logs the limit once, dispatches no new CLI sessions and pauses each affected
task after its process exits, with a phase such as codex usage limit until 15:30. Pausing closes the timed work
interval without losing progress. Once the reset time passes (or a newer snapshot clears the limit), the normal
paused-task path takes the task again automatically. Stale usage snapshots never block dispatch, so schedule
agent-status.py at least every five minutes as described in Host scripts.
A board Stop terminates only that task process. After a child exits, its slot becomes available and the journal
prints task <id>: agent session ended with status <code>. The worker reads only the JSON document of
--worker-json: a warning the board prints after it does not stop the loop.
Updating a running worker¶
A plain systemctl --user restart kills the agent sessions the worker started. The worker therefore keeps
itself current without one:
- New script on disk — a package release,
vendor:publish --tag=griglia-scripts, agit pull: within one interval the worker re-executes itself in place. Same PID, same lock, and every running session is handed over to the new code (--adopt), so nobody is interrupted; the journal printsreloading worker from <path> (<n> running session(s) carried over)and thencarried over after reload: …. A file that does not compile is ignored until it changes again. -
New environment —
~/.config/griglia-worker/<agent-key>.env, for exampleGRIGLIA_WORKER_MAX_PARALLEL: the service manager reads it at start, so the worker has to be restarted, but on its own terms. Send itSIGHUP:The worker starts no new session, lets the running ones finish, then exits;
Restart=alwaysin the unit starts it again with the current environment and script. The journal showsSIGHUP received: draining …anddrained: exiting so the service manager restarts the worker. New work opened meanwhile waits for the restart — a few seconds after the last session ends.
Use systemctl --user restart only when interrupting the running sessions is acceptable.
Check configuration without launching an agent:
python3 scripts/griglia-agent-worker.py --agent=codex --driver=codex --once --dry-run
python3 scripts/griglia-agent-worker.py --agent=codex --transport=local --php=/usr/bin/php8.4 \
--repo=/srv/my-project --once --dry-run
The command reads the board through the selected transport and prints the argv it would execute, so a failure here is a transport or permission problem, not an agent one.
For an end-to-end smoke test, enable the service, create a harmless task assigned to that agent and mark it
open to work. The journal should show dispatching task <id> to <agent> and the board should move from open,
to working, to done. Closing the terminal that started the test does not affect the systemd service.
Disable an instance with:
See also¶
- The agent side — commands, states and multi-agent scoping.
- Two agents at once — what two workers share, and how they avoid each other.
- Host scripts — all helpers published by
griglia-scripts. - Artisan commands — generated command reference.