Skip to content

feat: park status, append-only amendments, and schema cleanup (v0.4.0) - #8

Merged
TadMSTR merged 2 commits into
mainfrom
feat/park-amend-v04
Aug 2, 2026
Merged

TadMSTR merged 2 commits into
mainfrom
feat/park-amend-v04

Conversation

@TadMSTR

@TadMSTR TadMSTR commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes vikunja#293, #257, #164.

What changed

parked — a status, not a directory move

Quarantine relocated a task's YAML into a subdirectory that no reader listed, so a quarantined task vanished from the only interface that showed the queue — and the UI's confirm dialog promised a restore that was never implemented. Making park a status dissolves that instead of requiring a second feature to patch it.

  • Non-terminal, operator-only, reversible. park_task / unpark_task + POST /tasks/{id}/{park,unpark}.
  • parked_from records the prior status and is cleared on unpark, so unpark_task restores it without the caller knowing it.
  • Exempt from the TTL filter in list_tasks — a deliberate bookmark that silently expires is worse than no feature.
  • Verified against task-dispatcher.py as it exists today: its pickup loops match submitted (L561) and routing-failed (L834) only, and archive_expired() (L792) gates on TERMINAL_STATES. A parked task is invisible to all three, so no dispatcher change is needed.

Quarantine/restore removed with no migration or compat shim: the mechanism was never used — no quarantine/ dir on disk, no task ever carried status: quarantined, and no agent manifest granted either tool.

amend_task — append-only

A queued task's description was immutable, so anything learned between queuing and starting had nowhere to go, and an agent trusting its brief would act on stale instructions.

  • Amendments accumulate under payload.amendments; payload.description is never mutated.
  • Authorization is a trust boundary: only the task's source_agent or operator. The target agent is rejected — it must not rewrite the instructions it was handed, the same boundary that already makes cancelled operator-only.
  • Permitted on in-progress (the case that matters most); the response sets agent_may_have_started.
  • Bounded at 10 amendments / 4096 chars. More than one or two is a signal to cancel and re-queue — documented as a guideline in both README and AGENTS.md.

Repair path for out-of-vocabulary statuses

Two tasks on disk carry complete (not completed), written direct-to-YAML in May. Every mutation path rejected them — update_task needs the source in VALID_TRANSITIONS, set_task_status needs it in NON_TERMINAL_STATUSES, and complete is in neither. The queue has more than one writer, so the fix is to tolerate the unknown rather than widen the vocabulary: set_task_status can now move out of an unrecognised status given allow_override=True and a note, recording repaired_from. The target must still be valid.

GET /queue/summary

Counts by status behind the same _authorized() gate. Out-of-vocabulary statuses are bucketed under unknown rather than dropped, so records from other writers stay visible.

alert_state retired

The emitter was removed in July; nothing has read the block since. No longer written on create. Existing YAMLs keep theirs — the field is inert and rewriting hundreds of files to strip a no-op buys nothing. Deliberate, and a test pins that mutations preserve the residual block rather than silently dropping it.

Fixes

  • build-backend was invalid. setuptools.backends.legacy:build is not a real backend, so pip install -e ".[dev]" — the test setup AGENTS.md documents — failed with BackendUnavailable. Runtime was unaffected because the image installs from requirements.txt, which is exactly why it went unnoticed: CI installed from requirements.txt too, so nothing ever built the package. CI now installs from pyproject.toml so this class of breakage fails here in future.
  • The README published a weaker port bind than production runs — 8485:8485 (all interfaces) while the same document's Security section states the port is loopback-limited and that any process with loopback access can mutate the queue without the secret. Copying it verbatim exposed an unauthenticated mutation endpoint to the LAN. Now 127.0.0.1:8485:8485 with the reason stated inline.

Also

  • Python matrix narrowed to 3.11/3.12/3.13 and requires-python raised to >=3.11 — CI, pyproject.toml, the Dockerfile (3.12) and the fleet standard previously disagreed three ways.
  • ruff target-version = "py311" + tests/** per-file ignores, closing the fleet drift. This surfaced UP017, applied.
  • README sanitized for a public audience; amend_task added to the trust-model tool list — it is a new operator-mutating tool on the same unauthenticated loopback transport, and its source-agent check is an integrity control over a self-asserted actor, not authentication.

Verification

  • 122 tests, 91.9% coverage (was 84 / 90.7%).
  • pip install -e ".[dev]" verified in a scratch venv — broken on main today.
  • ruff check / ruff format --check clean.

Paired with cloudcli-plugin-task-queue#6, which needs this version's routes.

🤖 Generated with Claude Code

Replace the never-used quarantine mechanism with a `parked` status, add
`amend_task` so post-queue corrections reach the agent that will act on
them, and retire dead schema.

park as a status, not a directory move
  Quarantine relocated a task's YAML into a subdirectory no reader
  listed, so a quarantined task vanished from the only interface that
  showed it — and the confirm dialog promised a restore the UI never
  implemented. Making park a status dissolves that instead of needing a
  second feature to patch it. Parked tasks stay listed, are exempt from
  the TTL filter (a deliberate bookmark must not silently expire), and
  are skipped by the dispatcher, whose pickup loops match `submitted`
  and `routing-failed` only. `parked_from` records where to return to.

  Removed with no migration: the mechanism was never used — no
  `quarantine/` dir on disk, no task ever carried `status: quarantined`,
  no manifest granted either tool.

amend_task, append-only
  A queued task's description was immutable, so anything learned between
  queuing and starting had nowhere to go and an agent trusting its brief
  would act on stale instructions. Amendments accumulate under
  `payload.amendments`; the description is never mutated. Only the
  source_agent or operator may amend — the target agent is rejected, the
  same trust boundary that makes `cancelled` operator-only.

repair path for out-of-vocabulary statuses
  Two tasks on disk carry `complete` (not `completed`), written
  direct-to-YAML in May. Every mutation path rejected them. The queue
  has more than one writer, so tolerate the unknown rather than widen
  the vocabulary: `/queue/summary` buckets them, `set_task_status` can
  move out of one given an override plus a note.

Also fixes an invalid `build-backend` that made `pip install -e ".[dev]"`
fail — the documented test setup was broken. It survived because CI
installed from requirements.txt and so never built the package; CI now
installs from pyproject.toml. alert_state is no longer written on create
(emitter removed in July); existing YAMLs keep their inert block.

The README published a compose example binding 8485 on all interfaces
while the same document states the port is loopback-limited and that
loopback access alone can mutate the queue — corrected, with the reason
stated inline.

122 tests, 91.9% coverage.

Closes vikunja#293, #257, #164.

agent-id: developer
Audit task-queue-park-amend-2026-08: PASS, 0 Critical/High/Medium, 2 Low,
10 Info. Both Low findings confirmed risks identified during the build
rather than surfacing new ones; neither needs a code change. Recording
them where the next reader will hit them instead of only in the report.

unpark_task_handler outside-lock read — documented in the docstring. The
target is resolved before set_task_status_handler takes the same
non-reentrant fcntl lock, because holding it across both would deadlock.
An illegal transition still cannot land: that handler re-reads
current_status under the lock and re-validates. The residual race is a
redundant-but-valid transition plus a duplicate history entry.

parked joining NON_TERMINAL_STATUSES — documented above VALID_TRANSITIONS.
`failed` is the only transition target defined in terms of that derived
set, so adding a status silently widened it; the other three are literals
and did not. The real gap is that update_task_handler has no target_agent
ownership check at all, which is pre-existing and a behaviour change to
fix. Tracked in vikunja#325.

No functional change — comments and CHANGELOG only.

agent-id: developer
@TadMSTR
TadMSTR merged commit f79f89a into main Aug 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant