Files
mcpctl/docs/inference-tasks.md

239 lines
8.9 KiB
Markdown
Raw Permalink Normal View History

feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
# Inference Tasks (v5)
A **durable inference task queue**. Every inference call lands as a row
in the database; mcpd's previous in-memory request map is gone. mcpd
restart no longer drops in-flight work, worker disconnects no longer
fail callers, and pools that have no live workers can still accept
work that drains when one shows up.
The queue is the same persistence layer underneath two API surfaces:
- **Sync** (existing): `POST /api/v1/llms/<name>/infer` and
`POST /api/v1/agents/<name>/chat`. Caller's HTTP request stays open
until the task finishes, exactly like pre-v5. Internally it's now a
DB row with a 10-min wait. Used by `mcpctl chat` / `mcpctl chat-llm`.
- **Async** (new — this doc): `POST /api/v1/inference-tasks`. Caller
gets a task id immediately and polls / streams / cancels separately.
Useful for fire-and-forget scripts, queued work for offline pools,
and operator visibility into in-flight jobs.
## Lifecycle
```
┌──────► claimed ──first chunk──► running ──┐
pending ──┐ │ │ │
├──┘ on dispatch │ on revert (worker disconnect)│
│ └─────────► pending │
▼ ▼
cancelled ◄───────────── DELETE ──────────────► completed | error
```
| Status | Meaning |
|-------------|---------|
| `pending` | In queue, no worker has it yet (or claim was reverted) |
| `claimed` | A worker has it; dispatch frame went down the SSE channel |
| `running` | First chunk came back; streaming is in flight |
| `completed` | Worker POSTed terminal result |
| `error` | Permanent failure (auth, bad request, queue timeout) |
| `cancelled` | Caller cancelled via `DELETE` |
A worker disconnecting mid-task reverts `claimed`/`running` rows back
to `pending` (the row's `claimedBy` is cleared); a sibling worker on
the same pool drains it. Tasks survive mcpd restart — the row is the
source of truth, so a fresh mcpd reads the queue and either matches a
worker or waits.
## Async API
```
POST /api/v1/inference-tasks → 201 + { id, status, poolName, llmName, streaming, createdAt }
GET /api/v1/inference-tasks?... → list (owner-scoped by default; ?status, ?poolName, ?agentId, ?limit)
GET /api/v1/inference-tasks/<id> → poll one row
GET /api/v1/inference-tasks/<id>/stream → SSE: `event: chunk` + `event: terminal`
DELETE /api/v1/inference-tasks/<id> → cancel pending/claimed/running; no-op on terminal
```
Foreign-owner ids return **404** (not 403) on every endpoint — keeps
id-enumeration from leaking task existence across users.
### Enqueue
```sh
curl -X POST https://mcpd/api/v1/inference-tasks \
-H 'Authorization: Bearer ...' \
-H 'Content-Type: application/json' \
-d '{
"llmName": "vllm-local-qwen3",
"request": {
"messages": [{"role":"user","content":"summarize the changelog"}],
"max_tokens": 500
},
"streaming": false
}'
# 201 Created
# {
# "id": "cmoip...",
# "status": "pending",
# "poolName": "vllm-local-qwen3",
# "llmName": "vllm-local-qwen3",
# "streaming": false,
# "createdAt": "2026-04-28T..."
# }
```
Rejects with **400** for public Llms — the existing
`POST /api/v1/llms/<name>/infer` is the right tool there. Rejects
with **404** for missing Llm names.
### Poll
```sh
curl https://mcpd/api/v1/inference-tasks/cmoip... \
-H 'Authorization: Bearer ...'
# 200
# {
# "id": "cmoip...",
# "status": "completed",
# "responseBody": { "choices": [{"message":{"role":"assistant","content":"..."}}] },
# "claimedBy": "<providerSessionId>",
# "createdAt": "...", "claimedAt": "...", "completedAt": "..."
# }
```
### Stream live updates
```sh
curl -N https://mcpd/api/v1/inference-tasks/cmoip.../stream \
-H 'Authorization: Bearer ...'
# event: chunk
# data: {"data":"hello "}
#
# event: chunk
# data: {"data":"world","done":true}
#
# event: terminal
# data: { ...full row... }
```
Already-terminal tasks emit one `terminal` event and close. Caller
disconnect cleans up server-side.
### Cancel
```sh
curl -X DELETE https://mcpd/api/v1/inference-tasks/cmoip... \
-H 'Authorization: Bearer ...'
```
Returns 200 + the cancelled row. Cancelling a `completed` /
`error` / already-`cancelled` task is a no-op (returns the current
row). The worker that picked the task up may still execute the
underlying inference call (no remote-cancel protocol yet); the
result is just discarded server-side.
## CLI
docs(agentic-teams): document the new resources, and make the webhook reachable Five resources shipped with no documentation and one documented lie: docs/inference-tasks.md still told people `mcpctl get tasks` lists the inference queue. Work items own that name now. Adds docs/epics.md (the Epic -> EpicTask -> Task hierarchy, the state machine, risk-scaled review gates, the accept-merges gate, and reconciliation), docs/gitproviders.md (forge registration, discovery/import, merge-on-accept, post-merge reopen) and docs/agent-authority.md (the three run modes, and plainly which two are not implemented). Two things found while writing them, both fixed rather than filed: - `webhookSecretKey` was read by the webhook route but writable from NOWHERE -- absent from the create schema, update schema, service, CLI and apply spec. Since the endpoint fails closed on a null key, post-merge CI failures could never reopen their task; the board would keep claiming `merged` for work that broke the build. Adds --webhook-secret-key, the schema fields and the apply key, with tests covering persist / omit / explicit-null / reject-empty. Omission deliberately does not clear the column, so an unrelated edit cannot silently disarm the webhook. - src/cli/src/commands/create.ts contained a literal NUL byte inside a string literal (a binary-file check written with the raw character rather than an escape). Identical behaviour, but it made the whole file read as binary, so `grep -I` skipped it entirely and 1120 lines of CLI were invisible to every text search. Replaced with the escape. The Epic write path (criteria/status/tradeoffs) is API-only -- documented as such rather than papered over, and filed as task 180. Full suite 2612 green; lint at baseline 794/77. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-16 00:14:58 +01:00
> **Renamed.** This queue used to answer to `mcpctl get tasks`. Agentic-teams
> **work items** own that name now, so the queue keeps only its explicit
> spellings: `inference-tasks`, `inference-task`, `itasks`, `itask`. Bare
> `tasks`/`task` list work items — see [epics.md](epics.md).
>
> The same split applies to RBAC: this queue is authorized by
> `view:inference-tasks`, work items by `view:tasks`. Bindings written before
> the split are rewritten once at startup, so an existing `view:tasks` grant
> keeps working here and does **not** silently become a grant over work items.
feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
```sh
docs(agentic-teams): document the new resources, and make the webhook reachable Five resources shipped with no documentation and one documented lie: docs/inference-tasks.md still told people `mcpctl get tasks` lists the inference queue. Work items own that name now. Adds docs/epics.md (the Epic -> EpicTask -> Task hierarchy, the state machine, risk-scaled review gates, the accept-merges gate, and reconciliation), docs/gitproviders.md (forge registration, discovery/import, merge-on-accept, post-merge reopen) and docs/agent-authority.md (the three run modes, and plainly which two are not implemented). Two things found while writing them, both fixed rather than filed: - `webhookSecretKey` was read by the webhook route but writable from NOWHERE -- absent from the create schema, update schema, service, CLI and apply spec. Since the endpoint fails closed on a null key, post-merge CI failures could never reopen their task; the board would keep claiming `merged` for work that broke the build. Adds --webhook-secret-key, the schema fields and the apply key, with tests covering persist / omit / explicit-null / reject-empty. Omission deliberately does not clear the column, so an unrelated edit cannot silently disarm the webhook. - src/cli/src/commands/create.ts contained a literal NUL byte inside a string literal (a binary-file check written with the raw character rather than an escape). Identical behaviour, but it made the whole file read as binary, so `grep -I` skipped it entirely and 1120 lines of CLI were invisible to every text search. Replaced with the escape. The Epic write path (criteria/status/tradeoffs) is API-only -- documented as such rather than papered over, and filed as task 180. Full suite 2612 green; lint at baseline 794/77. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-16 00:14:58 +01:00
mcpctl get inference-tasks # table view: ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER
mcpctl get itasks # short alias
mcpctl get inference-task <id> # describe one
mcpctl get inference-task <id> -o yaml # full row, yaml format
feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
# Enqueue async via chat-llm:
mcpctl chat-llm vllm-local-qwen3 --async -m "summarize the changelog"
# stdout: cmoip... (just the id, pipeable)
# stderr: (task cmoip... enqueued for pool 'vllm-local-qwen3', status=pending)
# Wire to other commands:
ID=$(mcpctl chat-llm vllm-local-qwen3 --async -m "long task")
sleep 30
mcpctl get task $ID
```
## RBAC
feat(rbac): split the tasks namespace into tasks + inference-tasks v5 gave the durable inference queue the RBAC resource `tasks`, on the reasoning that inference was the only kind of task we had. Agentic teams introduces work-item tasks (Epic -> epictask -> task), which need that name. The queue moves to its own `inference-tasks` resource, matching its existing /api/v1/inference-tasks route prefix, and `task`/`tasks` now mean work items. Also lands the agentic-teams aliases: epic, epictask/et, repo/repository, session. The rename is not purely a code change. ResourceBindingSchema runs normalizeResource at parse time and stores the normalized string, so every RbacDefinition row written before the split literally contains "tasks" meaning inference. Shipping the alias change alone would silently stop those grants authorizing the queue and start them authorizing work items -- a privilege change in both directions. bootstrap/rbac-tasks-backfill.ts rewrites them once at startup, alongside migrateAdminRole. The run-once marker is claimed BEFORE the rewrite, not after. A failed marker write after a successful rewrite would let the next startup rewrite again, converting genuine post-split work-item grants into inference grants. Losing the rewrite is recoverable by re-granting; running it twice silently is not -- so if the marker cannot be persisted we skip the rewrite and warn loudly. mapUrlToPermission moves in lockstep: /api/v1/inference-tasks maps to `inference-tasks`, otherwise the split would break queue authorization on the first request. Adds generic getBooleanSetting/setSetting helpers to bootstrap/system-settings.ts, following the existing "global settings live in a Secret" pattern. setSetting refuses to write when the settings secret has been migrated onto a non-plaintext backend, where a direct `data` write would read back empty. Out of scope, tracked separately: the CLI still routes `mcpctl get tasks` to the inference queue (src/cli/src/commands/shared.ts). Repointing it needs the work-item resource to exist first, or the command breaks with no replacement. Tests: 14 unit tests cover alias resolution, the rewrite, wildcard and operation bindings being left alone, idempotency, and both marker-failure paths. Full workspace suite green (2291 tests). The new smoke test is RED until mcpd is deployed with this change -- it asserts post-split storage against a live daemon, and the currently deployed one predates the split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 10:10:53 +01:00
The `inference-tasks` resource. Default action mapping:
feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
| Method | Action |
|--------|--------|
| `GET` | `view` |
| `POST` | `create` |
| `DELETE` | `delete` |
feat(rbac): split the tasks namespace into tasks + inference-tasks v5 gave the durable inference queue the RBAC resource `tasks`, on the reasoning that inference was the only kind of task we had. Agentic teams introduces work-item tasks (Epic -> epictask -> task), which need that name. The queue moves to its own `inference-tasks` resource, matching its existing /api/v1/inference-tasks route prefix, and `task`/`tasks` now mean work items. Also lands the agentic-teams aliases: epic, epictask/et, repo/repository, session. The rename is not purely a code change. ResourceBindingSchema runs normalizeResource at parse time and stores the normalized string, so every RbacDefinition row written before the split literally contains "tasks" meaning inference. Shipping the alias change alone would silently stop those grants authorizing the queue and start them authorizing work items -- a privilege change in both directions. bootstrap/rbac-tasks-backfill.ts rewrites them once at startup, alongside migrateAdminRole. The run-once marker is claimed BEFORE the rewrite, not after. A failed marker write after a successful rewrite would let the next startup rewrite again, converting genuine post-split work-item grants into inference grants. Losing the rewrite is recoverable by re-granting; running it twice silently is not -- so if the marker cannot be persisted we skip the rewrite and warn loudly. mapUrlToPermission moves in lockstep: /api/v1/inference-tasks maps to `inference-tasks`, otherwise the split would break queue authorization on the first request. Adds generic getBooleanSetting/setSetting helpers to bootstrap/system-settings.ts, following the existing "global settings live in a Secret" pattern. setSetting refuses to write when the settings secret has been migrated onto a non-plaintext backend, where a direct `data` write would read back empty. Out of scope, tracked separately: the CLI still routes `mcpctl get tasks` to the inference queue (src/cli/src/commands/shared.ts). Repointing it needs the work-item resource to exist first, or the command breaks with no replacement. Tests: 14 unit tests cover alias resolution, the rewrite, wildcard and operation bindings being left alone, idempotency, and both marker-failure paths. Full workspace suite green (2291 tests). The new smoke test is RED until mcpd is deployed with this change -- it asserts post-split storage against a live daemon, and the currently deployed one predates the split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 10:10:53 +01:00
Aliases: `inference-task` and `inference-tasks` both normalize to
`inference-tasks`, so `mcpctl create rbac-binding --resource inference-task
--role view ...` works either way.
> **Renamed.** v5 shipped this queue under the RBAC resource `tasks`, on the
> reasoning that inference was the only kind of task we had. Agentic teams
> introduces work-item tasks, which take `tasks` (and the `task` alias) over.
> Bindings written before the split literally stored `"tasks"`, so mcpd rewrites
> them to `inference-tasks` once at startup — see
> `src/mcpd/src/bootstrap/rbac-tasks-backfill.ts`. Nothing to do by hand; the
> rewrite logs what it changed. `view:tasks` now grants **work items**, not
> inference.
feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
Owner scoping is enforced at the route layer ON TOP of the RBAC hook:
- A caller with only the implicit role sees and can cancel their own tasks.
feat(rbac): split the tasks namespace into tasks + inference-tasks v5 gave the durable inference queue the RBAC resource `tasks`, on the reasoning that inference was the only kind of task we had. Agentic teams introduces work-item tasks (Epic -> epictask -> task), which need that name. The queue moves to its own `inference-tasks` resource, matching its existing /api/v1/inference-tasks route prefix, and `task`/`tasks` now mean work items. Also lands the agentic-teams aliases: epic, epictask/et, repo/repository, session. The rename is not purely a code change. ResourceBindingSchema runs normalizeResource at parse time and stores the normalized string, so every RbacDefinition row written before the split literally contains "tasks" meaning inference. Shipping the alias change alone would silently stop those grants authorizing the queue and start them authorizing work items -- a privilege change in both directions. bootstrap/rbac-tasks-backfill.ts rewrites them once at startup, alongside migrateAdminRole. The run-once marker is claimed BEFORE the rewrite, not after. A failed marker write after a successful rewrite would let the next startup rewrite again, converting genuine post-split work-item grants into inference grants. Losing the rewrite is recoverable by re-granting; running it twice silently is not -- so if the marker cannot be persisted we skip the rewrite and warn loudly. mapUrlToPermission moves in lockstep: /api/v1/inference-tasks maps to `inference-tasks`, otherwise the split would break queue authorization on the first request. Adds generic getBooleanSetting/setSetting helpers to bootstrap/system-settings.ts, following the existing "global settings live in a Secret" pattern. setSetting refuses to write when the settings secret has been migrated onto a non-plaintext backend, where a direct `data` write would read back empty. Out of scope, tracked separately: the CLI still routes `mcpctl get tasks` to the inference queue (src/cli/src/commands/shared.ts). Repointing it needs the work-item resource to exist first, or the command breaks with no replacement. Tests: 14 unit tests cover alias resolution, the rewrite, wildcard and operation bindings being left alone, idempotency, and both marker-failure paths. Full workspace suite green (2291 tests). The new smoke test is RED until mcpd is deployed with this change -- it asserts post-split storage against a live daemon, and the currently deployed one predates the split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 10:10:53 +01:00
- `view:inference-tasks` resource-wide is required to see / list / cancel
feat(cli+docs+smoke): inference-task CLI + GC ticker + smoke + docs (v5 Stage 4) CLI surface for the durable queue: - `mcpctl get tasks` — table view (ID, STATUS, POOL, LLM, MODEL, STREAM, AGE, WORKER). Aliases `task`, `tasks`, `inference-task`, `inference-tasks` all normalize to the canonical plural so URL construction works uniformly. RESOURCE_ALIASES + completions generator updated. - `mcpctl chat-llm <name> --async -m <msg>` — enqueue and exit. stdout is just the task id (pipeable into `xargs mcpctl get task`); stderr carries human-readable status. REPL mode is rejected for --async (fire-and-forget doesn't make sense without -m). GC ticker in mcpd: 5-min interval. Pending tasks past 1 h queue timeout flip to error with a clear message; terminal tasks past 7 d retention get deleted. Both queries are index-backed. Crash fix uncovered by the smoke: when the async route doesn't await ref.done, a later cancel/error rejected the in-flight Promise as unhandled and crashed mcpd. The route now attaches a no-op `.catch` so the legacy `done` semantic still works for sync callers (chat, direct infer) without taking out the process for async ones. The EnqueueInferOptions also gained an explicit `ownerId` field so the async API can stamp the authenticated user on the row instead of inheriting 'system' from the constructor's resolveOwner — without this, every GET/DELETE from the original caller would 404 due to foreign-owner mismatch. Smoke (tests/smoke/inference-task.smoke.test.ts): 1. POST /inference-tasks while no worker bound → row=pending. 2. Bring a registrar online → bindSession drain claims and dispatches → worker complete()s → row=completed → GET returns the assistant body. 3. Stop worker, enqueue, DELETE → row=cancelled, persisted. docs/inference-tasks.md (new): full data model, lifecycle diagram, async API reference, CLI examples, RBAC table, GC defaults, and the v5 limitations / v6 roadmap. Cross-linked from virtual-llms.md and agents.md. Tests + smoke: mcpd 893/893, mcplocal 723/723, cli 437/437, full smoke 146/146 (was 144, +2 new task smoke). Live mcpd verified via manual curl: enqueue → cancel → re-fetch — no crash, owner scoping returns 404 on foreign ids, GC ticker logs at info when it sweeps. v5 complete: durable queue (Stage 1) + VirtualLlmService rewire (Stage 2) + async API & RBAC (Stage 3) + CLI/GC/smoke/docs (Stage 4).
2026-04-28 15:25:09 +01:00
another user's tasks.
## GC
mcpd runs a 5-min ticker that:
- Flips **pending** tasks older than the queue timeout (default 1 h)
to `error` with a clear message ("Task expired in pending state
after ...s"). Any caller still polling sees a clean failure.
- Deletes **terminal** tasks (`completed`/`error`/`cancelled`) past
the retention window (default 7 d). Indexes back the
`WHERE status IN (...) AND completedAt < cutoff` predicate so the
sweep is cheap even with millions of rows.
Both timeouts are constants in `main.ts`; making them per-deployment
configurable is a v6 concern.
## Limitations / Out-of-scope for v5
- **Streaming chunks are not persisted.** The DB stores only the
final assembled body (or null for streaming-only consumers). A
caller who connects to `/stream` AFTER the task completed gets one
`terminal` event with no chunk replay. Persisting deltas would be
a per-chunk INSERT — too expensive at SSE granularity.
- **Public-Llm queueing** is not exposed. Public Llms are HTTP
adapters with no SSE worker, so the queue concept doesn't apply.
Direct `/llms/<name>/infer` against a public Llm stays the right
call.
- **Multi-instance mcpd**: the in-process `EventEmitter` that wakes
blocked HTTP handlers fires only on the mcpd that holds the row's
blocked handler. With multiple mcpd replicas behind a load balancer
the wakeup wouldn't cross instances. v6 swap: pg `LISTEN/NOTIFY`,
no schema change.
- **Worker capacity** is one task per session at a time (today). A
per-session capacity hint + scheduler-level concurrency control
is a v6+ concern.
- **Remote cancel protocol**: cancelling a `running` task marks the
row as cancelled but the worker keeps executing. The result POST
is discarded server-side. Real cancellation (an SSE message that
tells the worker to abort its in-flight call) is a v6 concern.
## See also
- [virtual-llms.md](./virtual-llms.md) — the Llm-side primitives
(kind, lifecycle, pools) that the queue routes around.
- [agents.md](./agents.md) — how the chat path uses the same queue
underneath.