Repository navigation
Description
Summary
mcpx's task store (pkgs/mcpx/internal/tasks) starts a task's TTL clock at creation, but the TTL is documented — in mcpx's own configuration reference — as retention for a finished task: tasks.ttl "governs: how long a finished task's result is kept" (pkgs/mcpx/docs/configuration.md:618, default 10m0s). The store instead expires a task ttl after it was started, whatever state it is in:
tasks.go:389-395—Startarmstime.AfterFunc(ttl)unconditionally; when it fires it deletes the task from the in-memory map and the DB and callscancel(), killing the work's context mid-run.tasks.go:210,254-255— the persistent reload path computes the same creation-based deadline (expiresAtMs := createdMs + ttlMs) and re-arms deletion for the remainder, so a restart does not rescue the semantics.
Verified on main adb8ceaa with a probe test in the real internal/tasks package ON THE LAPTOP (detached worktree, removed after; tasks.go sha256 3d274167… identical in both clones; probe + log in the fleet bookkeeping probe-2026-10-28-laneA-taskttl/, run twice with identical output; the full existing internal/tasks suite passes). Control: a 50 ms task under a 5 s TTL completes and stays collectable.
Bug A — a task still Working at its TTL is deleted and cancelled mid-run
Probe: TTL 300 ms, work that needs 900 ms:
- at +500 ms (work still running)
Get(id)→ missing; - the work's context was cancelled at +301 ms;
- after the work finishes anyway,
Result(id)→ErrNoTask("no task …; it may have expired") — the produced result is uncollectable, and a polling client cannot distinguish "the work was killed by its own retention clock" from "you invented that handle".
This is the exact workload the package exists for — its doc: a task is "the right shape for a genuinely slow call — a build, a crawl, a browser session — where holding a request open for minutes invites every intermediary to time it out". With the default 10-minute TTL, any task-shaped call that legitimately runs longer than 10 minutes is cancelled by the store itself, and a client that requested a small ttl (e.g. the 1000 ms values used in the server's own tests) gets its still-running call killed almost immediately.
Bug B — a finished task's retention shrinks by however long it ran
Probe: TTL 1000 ms, work that runs 800 ms and completes. At finish+300 ms the task is already gone. A task that runs 9m59s of a 10-minute TTL keeps its finished result for ~1 second; the client's window to collect tasks/result races the deletion regardless of how promptly it polls after completion. The retention a caller actually gets is ttl − runtime, not the documented ttl.
Call-site nuance for triage
One caller passes a deadline-flavoured TTL: internal/daemon/input_required.go:100 starts the parked /v1/call task with proto.askTTL (15 m, "how long the daemon keeps the call and its result", docs/protocol.md:559). If that call site relies on expiry to end an unanswered call, the fix must give it an explicit runtime deadline rather than quietly keeping today's overloaded meaning; the other call sites (internal/mcpserver/tasks.go:74, internal/daemon/routes_exec.go:245, internal/daemon/routes_proto.go:434, internal/daemon/ops.go:525) pass plain retention values from request task.ttl / tasks.ttl.
Acceptance criteria
- A task still in
working/input_requiredat its TTL is not deleted and its context is not cancelled by the retention clock; it runs to a terminal state. - A terminal task is retained for a full TTL measured from when it reached the terminal state (finish time), in memory and across a store reopen (the reload path computes remaining retention from the finish/
lastUpdatedAtof a terminal task, not fromcreated_at). - Regression tests in
internal/taskscover both probe shapes: (a) work outliving its TTL completes and its result is collectable; (b) a task that runs most of its TTL still keeps its result for the full TTL after finishing. - The
input_requiredask-deadline behaviour is either preserved through an explicit mechanism or explicitly re-ruled, with the call site anddocs/protocol.mdupdated to match. Task.TTL's doc comment (tasks.go:39, currently "milliseconds after creation that the result is kept") and any other doc that describes the clock is updated to state the ruled semantics.- All existing
internal/taskstests keep passing, includingTestTaskPersistenceAcrossReopenand the cancellation tests.
Activity
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
34 remaining items
dezren39 commentedon Oct 31, 2026
Agent note — pickup (resume)
final: CI time budgetstill queued.mcpx(tasks): clock TTL retention from the terminal state, not creation (
dezren39 commentedon Oct 31, 2026
Agent note — done
go test ./internal/tasks/... ./internal/daemon/...PASS.final: CI time budgetmeta-check was still queued at merge time (queued since 03:13Z while every substantive leg completed); noted here as CI-queue congestion, not a change failure.dezren39 commentedon Oct 31, 2026
Agent note — correction
queue/4163-1653-tasks-ttl-terminal; the PR's actual head branch isqueue/4163-1653-tasks-ttl-retention. Everything else in the record (PR mcpx(tasks): clock TTL retention from the terminal state, not creation (#1653) #2923, merge sha b90f91a) is correct.