Skip to content

mcpx(tasks): TTL is clocked from creation, not completion — a task still working at its TTL is deleted and cancelled mid-run, and a finished task's retention shrinks by its runtime #1653

Closed

Description

@dezren39

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 — Start arms time.AfterFunc(ttl) unconditionally; when it fires it deletes the task from the in-memory map and the DB and calls cancel(), 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_required at 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/lastUpdatedAt of a terminal task, not from created_at).
  • Regression tests in internal/tasks cover 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_required ask-deadline behaviour is either preserved through an explicit mechanism or explicitly re-ruled, with the call site and docs/protocol.md updated 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/tasks tests keep passing, including TestTaskPersistenceAcrossReopen and the cancellation tests.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job, lane A (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a cron run, 2026-10-28 06:05 CDT
    • Found via: filed this run
    • Task: Triage the task-store TTL clock (retention measured from creation, deleting + cancelling still-working tasks) and pin the ruled semantics, including the input_required askTTL call site
    • Expectation: effort/s — done looks like the acceptance criteria in the issue body are ruled on and a fix task is scoped
    • Links: pkgs/mcpx/internal/tasks/tasks.go (Start expiry :389-395, reload expiry :210/:254-255), pkgs/mcpx/docs/configuration.md:618
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  4. 34 remaining items

  5. added
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note — pickup (resume)

  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note — done

    • Task: 4163 complete. PR mcpx(tasks): clock TTL retention from the terminal state, not creation (#1653) #2923 merged @ b90f91a (squash), closing this issue.
    • Final state at merge: lint / plugin tests / stress changed-tests / mcpx-docs build green on 9202182; the one Codex P2 (ask waiting deadline) was fixed in 9202182, answered on-thread, and formally resolved this shift before merging. Local verification (prior shift): go test ./internal/tasks/... ./internal/daemon/... PASS.
    • The final: CI time budget meta-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.
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note — correction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions