Skip to content

Add GET /extractions: list the caller's own non-terminal jobs (stranded-job recovery for the cutover health check) #17

Description

@TBarregren

Summary

Add GET /kntnt-extractor/v1/extractions (no id) — list the caller's own non-terminal extraction jobs — so a client can find and cancel a job stranded by a crashed earlier run.

This restores a belt-and-braces the kntnt-wp-skills#24 cutover otherwise loses. kntnt-wp-skills's health check historically swept stranded remote workspaces from an aborted earlier session (its user story 9), so a crashed run never left something behind that blocked the next one.

The problem it solves

Extraction concurrency defaults to one global non-terminal job (max_active_jobs). If a run crashes after POST /extractions but before consume/cancel, that job sits queued/running/ready and holds the only slot until the TTL heartbeat window reclaims it — one hour by default (Sweeper::DEFAULT_TTL). Until then, the next run's POST /extractions is refused 429.

Today the client cannot clear it: jobs are addressable only by their 32-hex id (which a crashed run may never have persisted), and they are owner-bound with no way to enumerate them. So a single crash locks the operator out for up to an hour, with no recourse but to wait.

A listing of the caller's own live jobs lets the health check detect the stray and DELETE /extractions/{id} it — exactly the sweep the old flow did, now within the plugin's ownership model.

Decision J1 — what to list

Recommended: the caller's own non-terminal jobs only (queued, running, ready) — the ones that hold the slot and are the recovery target. Terminal jobs (consumed/failed/cancelled/expired) are done and irrelevant to the stranded-slot problem, and listing them turns this into an audit surface the audit-log endpoint already owns.

A ?state= filter or an ?include_terminal=true option is a clean later extension if a use-case appears; not needed now.

Proposed contract

GET /kntnt-extractor/v1/extractions          (permission: shared Authorizer — both caps, 403 otherwise)
200 →
{
  "jobs": [
    {
      "id": "0a1b2c...",              // 32-hex
      "state": "ready",              // queued | running | ready (non-terminal only)
      "created_at": 1737500000,      // unix seconds, as persisted
      "updated_at": 1737500123,
      "progress": { "tables_done": 2, "tables_total": 2, "files_done": 1, "files_total": 3 }  // optional, same shape as poll
    }
  ]
}
  • Owner-scoped: Job_Store::all() filtered to owner === get_current_user_id(). A caller never sees another user's jobs, so this does not weaken the ownership model or become an existence oracle for others' jobs.
  • No download_url — fetching an artifact stays the per-job GET /extractions/{id} poll's job; this is a slot-management listing, not a delivery path.
  • Empty jobs array when the caller has none.

The considered alternative — just raising max_active_jobs so a stray does not block the next run — is a config stopgap that never cleans the stray up (it keeps consuming disk and its slot until TTL) and does not give the operator recourse. Rejected as the primary fix; it remains available as an independent tuning knob.

Acceptance criteria

  • AC1 — an authorized GET /extractions returns the caller's own non-terminal jobs, each with id, state, created_at, updated_at (and progress where the job has advanced).
  • AC2 — terminal jobs (consumed/failed/cancelled/expired) are omitted.
  • AC3 — a job created by a different user never appears in this caller's listing.
  • AC4 — an anonymous or single-capability caller is refused 403 (same gate as /tables).
  • AC5 — the listing is { "jobs": [] } when the caller has no live jobs.
  • AC6 — after the caller consumes or cancels a listed job, it no longer appears.
  • AC7 — increments the REST API version to 2 (a new caller-visible endpoint). (Single bump to 2 if the sibling cutover tickets ship together.)

Testing (test-first)

  • Playground integration: create a job → GET /extractions lists it with the right shape; cancel it → it drops from the listing; a capable non-owner's listing omits it (reuse the ownership-binding setup from extractions-test.php); an unauthorized caller gets 403.

References

  • Origin: kntnt-wp-skills#24 cutover; restores that project's user story 9 (no stranded workspace outlives a crashed session) within this plugin's ownership + TTL model.
  • ADR-0002 (both-caps Authorizer gate), ADR-0004 (job lifecycle / concurrency slot / TTL sweep — the 1-hour default window this endpoint lets the caller short-circuit), ADR-0005 (API version).
  • Code: classes/Rest/Extractions_Controller.php (add the collection GET route + callback), classes/Job_Store.php (all() already exists; filter by owner), classes/Job_State.php (is_terminal()).

Ready for agent. J1 has a clear recommended default (non-terminal, owner-scoped) with a documented optional extension; no blocking open question. The only cross-ticket coordination is the shared API-version bump to 2 if the three kntnt-wp-skills#24 cutover tickets ship in one release.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions