Skip to content

feat: Watch Management API — REST + ov CLI + MCP (RFC #2104)#2110

Merged
qin-ctx merged 9 commits into
volcengine:mainfrom
t0saki:feat/watch-management-api
May 19, 2026
Merged

feat: Watch Management API — REST + ov CLI + MCP (RFC #2104)#2110
qin-ctx merged 9 commits into
volcengine:mainfrom
t0saki:feat/watch-management-api

Conversation

@t0saki
Copy link
Copy Markdown
Collaborator

@t0saki t0saki commented May 18, 2026

Description

Implements RFC #2104 — Watch Management API across all three control planes (REST / ov CLI / MCP). Watch tasks created via POST /resources with watch_interval previously had no list / pause / delete / trigger / update endpoint — this PR closes that gap.

Zero backend changes. WatchManager already exposes every primitive needed. This PR is purely a router / MCP-tool / CLI-command layer.

Related Issue

Implements RFC discussion: #2104

(See the v2 follow-up comment for the 4 design deltas finalized during PR review — dual-key relaxation, fire-and-forget trigger, CLI nesting under ov task, and the set-intervalupdate rename.)

Type of Change

  • New feature (non-breaking change that adds functionality)

Changes Made

P1 + P2 — REST /api/v1/watches (openviking/server/routers/watches.py):

  • GET /watches — list (with ?active_only=); also acts as single-lookup via ?to_uri=. When both are supplied a paused task at that URI also 404s, keeping the active_only contract consistent across branches.
  • GET /watches/{task_id} (with optional ?to_uri= cross-key sanity check)
  • PATCH /watches/{task_id} or ?to_uri= — partial update of watch_interval, is_active, reason, instruction. is_active is orthogonal to watch_interval for pause-without-losing-cadence.
  • DELETE /watches/{task_id} or ?to_uri=
  • POST /watches/{task_id}/trigger or /watches/trigger?to_uri=fire-and-forget, returns immediately. Background task held via a module-level _BACKGROUND_TRIGGER_TASKS: set with add_done_callback(discard) so the asyncio loop's weak-ref policy doesn't GC the refresh mid-execution.
  • Dual-key semantics: when both {task_id} and ?to_uri= are supplied, accept if they refer to the same task (cross-key sanity check); reject with 400 only on disagreement.

P3 — MCP add_resource gains watch params (openviking/server/mcp_endpoint.py):

  • Net-new watch_interval: float = 0 + to: str = "" parameters. Negative values rejected with a clear error; watch_interval > 0 requires to (server-side hard constraint with hint message for agents). Main branch's MCP had no watch entrypoint.

P4 — MCP minimum closure (openviking/server/mcp_endpoint.py):

  • list_watches() — one line per task, no task_id exposed (agents use URI)
  • cancel_watch(to_uri) — delete by URI; idempotent on race-removal
  • Pause/resume/trigger/update are intentionally NOT in MCP — those are power-user operations belonging on the CLI; keeps agent system prompt small.

P5 — ov task watch subcommand group (crates/ov_cli/src/commands/watch.rs):

  • Nested under ov task (sibling of ov task status / ov task list) since watch is a form of background work tracking, parallel to single-execution task records.
  • Subcommands: ls [--active-only], show, rm, pause, resume, update, trigger
  • update <key> [--interval N] [--active true|false] [--reason ...] [--instruction ...] unifies all four mutable PATCH fields (replacing the original set-interval single-field verb). At-least-one-flag enforced.
  • <key> auto-classification (classify_key helper): viking:// prefix → by-URI route; other-scheme :// keys (http://, Viking://, etc.) rejected locally with a clear Parse error instead of silently 404ing server-side.
  • Adds BaseClient::patch and BaseClient::post_with_query (PATCH was absent; the latter supports POST .../trigger?to_uri=).

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

Test coverage:

  • tests/server/test_api_watches.py (new, 11 tests) — empty list, full lifecycle, by-URI lookup, active_only filter (both branches), dual-key matching accepted, dual-key mismatch 400, missing key (422), 404 cases, extra-fields rejection (422), trigger-by-URI fire-and-forget timing, partial PATCH preserves unset fields
  • tests/server/test_mcp_endpoint.py (extended, 6 new tests) — watch_interval requires to, negative watch_interval rejected, list_watches empty + with seed, cancel_watch by URI, cancel_watch not-found
  • crates/ov_cli/src/commands/watch.rs — Rust unit tests for classify_key (uri/task_id/case-insensitive reject/other-scheme reject) and validate_interval_minutes and build_update_body (no-flag rejection / full-field / partial-field / interval validation). 10 tests total.
  • cargo build -p ov_cli clean; cargo test -p ov_cli commands::watch:: passes
  • uvx ruff format --check + uvx ruff check clean on all modified files

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas (PermissionDeniedError class duplication, dual-key resolution, 404-collapse-on-no-permission rationale, fire-and-forget task ref retention)
  • I have made corresponding changes to the documentation (RFC [RFC] Watch Management API (REST + ov CLI + MCP) #2104 carries the design; a v2 delta comment records the post-review adjustments. Per-endpoint OpenAPI is auto-generated from FastAPI signatures.)
  • My changes generate no new warnings
  • Any dependent changes have been merged and published (none — backend unchanged)

Additional Notes

Key design decisions (full rationale in RFC #2104 + the v2 delta comment):

  1. Dual-key REST: every single-resource endpoint accepts either {task_id} in path or ?to_uri= in query. WatchManager already maintains both indexes, so zero added cost. When both are supplied they must refer to the same task — useful as a cross-key sanity check from clients with both pieces of information; only mismatch returns 400.
  2. PermissionDeniedError translation: openviking/resource/watch_manager.py:104 defines its own PermissionDeniedError(Exception) which is NOT a subclass of OpenVikingError. Without explicit translation in the router, it falls through the global exception handler as 500. Each REST handler catches wm_mod.PermissionDeniedError and re-raises openviking_cli.exceptions.PermissionDeniedError so the global handler returns 403.
  3. DELETE returns 404 on missing: WatchManager.delete_task returns bool; False is translated to NotFoundError rather than {ok: false}, avoiding contract drift for CLI consumers.
  4. 404 vs 403 on cross-tenant access: _resolve_task deliberately collapses "doesn't exist" and "no permission" into 404 to avoid leaking task existence to unauthorized callers — a security-first stance documented inline.
  5. Fire-and-forget trigger: WatchScheduler.schedule_task awaits _execute_task inline (full re-ingest, can take many seconds). The HTTP handler dispatches via asyncio.create_task and returns immediately. Strong refs held in _BACKGROUND_TRIGGER_TASKS: set because asyncio only retains weak refs.
  6. CLI nesting: ov task watch * rather than top-level ov watch *. Watch (recurring subscription) and task (single-execution record) are both background-activity tracking concepts; co-locating them simplifies the top-level command tree.
  7. update over set-interval: the underlying PATCH covers four fields; one update subcommand with flags scales better than a set-X verb per field. pause/resume retained as high-frequency shortcuts.
  8. Three-tier strategy: REST = full functionality; ov CLI = REST parity for power users; MCP = minimum closure (add + list + cancel) to control agent system-prompt size and decision noise.

Out of scope (RFC §6 Open Questions, follow-ups):

  • Trigger rate-limiting
  • List pagination
  • Batch operations
  • POST /resources.watch_interval long-term deprecation
  • Audit logging via telemetry

Commit layout (logical phases, for reviewer-friendly diffing):

  1. feat(mcp): add watch_interval + to params to add_resource (P3)
  2. feat(server): add REST /watches management endpoints (P1+P2)
  3. feat(mcp): add list_watches and cancel_watch tools (P4)
  4. feat(cli): add ov watch subcommand group (P5)
  5. fix: address Copilot review on PR #2110
  6. fix: address second round of Copilot review on PR #2110
  7. fix: address third round of Copilot review on PR #2110
  8. refactor(cli): nest watch under task; replace set-interval with update

t0saki added 4 commits May 18, 2026 15:43
Net-new MCP capability — main branch's add_resource tool had no watch
entrypoint. Watch_interval > 0 triggers periodic full re-ingest via
existing WatchScheduler, requires explicit `to` URI to avoid collision
in WatchManager._uri_to_task reverse index.

Part of P3 of RFC volcengine#2104 (Watch Management API).
Implements P1+P2 of RFC volcengine#2104 (Watch Management API).

New /api/v1/watches router exposing:
- GET /watches (list, with active_only filter; supports ?to_uri= for single
  lookup)
- GET /watches/{task_id}
- PATCH /watches/{task_id} or ?to_uri= (partial update: watch_interval,
  is_active, reason, instruction — orthogonal pause via is_active)
- DELETE /watches/{task_id} or ?to_uri=
- POST /watches/{task_id}/trigger or /watches/trigger?to_uri= (immediate
  refresh, does not wait)

Reuses WatchManager primitives directly — zero backend changes. Translates
watch_manager.PermissionDeniedError (plain Exception) to the
OpenVikingError-rooted variant so the global handler renders 403.

Tests in tests/server/test_api_watches.py cover empty list, full
lifecycle, by-uri lookup, active_only filter, dual-key behavior, missing
key, 404, extra-fields rejection, trigger-by-uri, and partial PATCH.
Implements P4 of RFC volcengine#2104. MCP gets the minimum closure: list + cancel.
Pause/resume/trigger/set-interval are intentionally not exposed — those
are power-user operations belonging on the ov CLI, not in the agent's
system prompt.

cancel_watch uses to_uri as the primary key (agents don't track UUIDs).
list_watches returns a compact one-line-per-task format with URI,
interval, status, and next-run timestamp.

Tests cover empty/seeded list, cancel-by-URI, cancel-not-found, plus the
add_resource watch_interval-without-to error hint added in the prior
commit on this branch.
Implements P5 of RFC volcengine#2104. Full parity with the REST /watches surface:

  ov watch ls [--active-only]
  ov watch show <key>
  ov watch rm <key>
  ov watch pause <key>
  ov watch resume <key>
  ov watch set-interval <key> <minutes>
  ov watch trigger <key>

<key> auto-detects by prefix: a "viking://" string routes to the by-uri
HTTP endpoint, anything else is treated as a task_id and routes to the
path endpoint. Saves users from remembering UUIDs.

Adds patch() and post_with_query() helpers to BaseClient (PATCH was
absent; post_with_query supports POST .../trigger?to_uri=...). Output
flows through the existing OutputFormat (Table / JSON) helper unchanged.
@github-actions
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🏅 Score: 95
🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Missing License Header

New Rust file crates/ov_cli/src/commands/watch.rs does not include the required AGPL-3.0 license header. All new source files in this project should contain the license header at the top.

// Watch management subcommand handlers (RFC #2104).

@github-actions
Copy link
Copy Markdown

PR Code Suggestions ✨

No code suggestions found for the PR.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements RFC #2104 — Watch Management API across three control planes (REST, MCP, and the ov CLI). The change is a thin layer over existing WatchManager primitives: new REST endpoints under /api/v1/watches with dual-key (path {task_id} or ?to_uri=) resolution, two new MCP tools (list_watches, cancel_watch) plus watch_interval/to params on add_resource, and a new ov watch subcommand group with full REST parity. Tests cover the REST lifecycle and MCP tool variants; a Rust unit test validates URI-vs-task-id key classification.

Changes:

  • Adds REST routes (list/get/patch/delete/trigger) with dual-key support, permission-error translation, and partial-update semantics.
  • Extends MCP add_resource with watch_interval/to, and adds list_watches/cancel_watch tools.
  • Adds ov watch ls/show/rm/pause/resume/set-interval/trigger CLI plus patch/post_with_query HTTP client helpers.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
openviking/server/routers/watches.py New router implementing REST /watches lifecycle with dual-key resolution and exception translation.
openviking/server/routers/init.py Exports the new watches_router.
openviking/server/app.py Registers the watches router on the FastAPI app.
openviking/server/mcp_endpoint.py Adds watch_interval/to to add_resource and new list_watches/cancel_watch MCP tools.
crates/ov_cli/src/main.rs Wires the new Watch subcommand group into the CLI dispatcher.
crates/ov_cli/src/commands/mod.rs Declares the new watch command module.
crates/ov_cli/src/commands/watch.rs New CLI handlers for ls/show/rm/pause/resume/set-interval/trigger plus is_uri classifier and unit tests.
crates/ov_cli/src/client.rs Adds typed list/get/patch/delete/trigger_watch_* helpers and exposes generic patch/post_with_query.
crates/ov_cli/src/base_client.rs Adds low-level patch and post_with_query methods with query-string + JSON body support.
tests/server/test_api_watches.py New end-to-end tests covering the REST lifecycle, by-URI lookup, active filtering, and edge cases.
tests/server/test_mcp_endpoint.py Adds tests for add_resource watch requirement and the new MCP list_watches/cancel_watch tools.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/server/test_api_watches.py Outdated
Comment thread tests/server/test_api_watches.py Outdated
Comment thread crates/ov_cli/src/commands/watch.rs Outdated
Comment thread openviking/server/mcp_endpoint.py
Comment thread openviking/server/mcp_endpoint.py
Comment thread openviking/server/mcp_endpoint.py Outdated
Comment thread openviking/server/routers/watches.py
Comment thread openviking/server/routers/watches.py Outdated
REST router (`watches.py`):
- _resolve_task: relax dual-key handling. When both {task_id} and ?to_uri=
  are supplied, accept if they refer to the same task (useful cross-key
  check), reject only on disagreement. Expose ?to_uri= on the by-id GET /
  PATCH / DELETE / trigger routes so this is reachable from HTTP.
- _patch_impl: drop fragile "not found" substring matching. _resolve_task
  pre-checks existence; if WatchManager.update_task still raises ValueError
  (race window), map to 404 — matches the pre-check behavior. Add a
  docstring explaining why ConflictError and other OpenVikingError-rooted
  exceptions are intentionally allowed to bubble for the global handler.

MCP (`mcp_endpoint.py`):
- add_resource: reject negative watch_interval. Previously a negative value
  silently bypassed the `watch_interval > 0 requires to` check and was
  forwarded with undefined semantics.
- cancel_watch: treat delete_task returning False as idempotent success.
  Race-removal between our lookup and delete is a benign concurrent cancel
  — the desired post-condition holds either way.

Tests:
- Rename test_dual_key_conflict_returns_400 → test_dual_key_matching_accepted
  (the matching case is now valid). Add test_dual_key_mismatch_returns_400
  for the genuine conflict path.
- Rename test_patch_to_uri_conflict_returns_409 → test_patch_rejects_unknown_field
  to match what it actually verifies.

CLI (`commands/watch.rs`):
- set_interval: local-validate `minutes > 0` before issuing the HTTP request
  so we fail fast with a clear message instead of a generic server 400.

Addresses: comments 1, 2, 3, 4, 6, 7, 8 on PR volcengine#2110.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Comment thread tests/server/test_api_watches.py Outdated
Comment thread openviking/server/routers/watches.py
Comment thread openviking/server/routers/watches.py
Comment thread openviking/server/mcp_endpoint.py Outdated
Comment thread openviking/server/mcp_endpoint.py
Comment thread crates/ov_cli/src/commands/watch.rs Outdated
Comment thread crates/ov_cli/src/commands/watch.rs
Comment thread openviking/server/routers/watches.py
REST router (`watches.py`):
- _trigger_impl: dispatch schedule_task via asyncio.create_task so the
  HTTP request returns immediately. WatchScheduler.schedule_task awaits
  _execute_task inline (full re-ingest can take many seconds); keeping the
  trigger truly fire-and-forget matches the documented contract.
- _resolve_task: document the deliberate 404-for-no-permission collapse
  (avoids leaking task existence to unauthorized callers across tenants).
- by-id route ?to_uri= Query: expand description to warn that a wrong
  cross-key value blocks the operation with 400.

MCP (`mcp_endpoint.py`):
- list_watches: drop dead `try/except PermissionDeniedError` — WatchManager
  .get_all_tasks silently filters and does not raise. Replace with a
  comment pointing at the filtering line.
- cancel_watch: rename captured-but-unused `ok` to `_` and clarify the
  inline comment to state that the return value is intentionally ignored
  (idempotent on the race-removal case).

CLI (`commands/watch.rs`):
- Extract `classify_key` and `validate_interval_minutes` as pure helpers,
  unit-tested directly. The previous "boolean predicate" test gave false
  coverage — would have passed even if the guard was removed.
- classify_key now rejects `Viking://` (wrong case) and other-scheme `://`
  inputs (e.g. `http://`) with a clear Parse error, instead of silently
  treating them as task_ids that 404 server-side.

Tests:
- test_full_lifecycle and test_trigger_by_uri: fix monkeypatch signature
  (`(self, task_id)` instead of `(task_id)`) — class-level setattr binds
  self. Wait on an asyncio.Event so the assertion does not race the
  background asyncio.create_task started by the new fire-and-forget
  trigger implementation.

Addresses: comments 9, 10, 11, 12, 13, 14, 15, 16 on PR volcengine#2110.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread openviking/server/routers/watches.py Outdated
Comment thread openviking/server/routers/watches.py
Comment thread openviking/server/mcp_endpoint.py
REST router (`watches.py`):
- Hold strong references to fire-and-forget trigger tasks. The event loop
  only keeps weak refs to tasks created via asyncio.create_task, so a
  discarded handle can be garbage-collected mid-execution and silently
  abort the refresh. Add a module-level `_BACKGROUND_TRIGGER_TASKS` set
  and use add_done_callback(set.discard) to release on completion.
- list_or_get_watch: when `?to_uri=` and `active_only=true` are both
  supplied, also 404 paused tasks. Previously the to_uri branch returned
  any matching task regardless of active_only, which was inconsistent
  with the list-branch contract.

Tests:
- Add test_add_resource_rejects_negative_watch_interval covering the
  guard introduced in the prior commit.

Addresses: comments 17, 18, 19 on PR volcengine#2110.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Two ergonomic changes after PR review feedback:

1. `ov watch *` → `ov task watch *`. The top-level `Watch` command moved
   under `TaskCommands` since both are forms of background work tracking
   (watch = recurring subscription, task = single-execution record). REST
   stays at /api/v1/watches — only the CLI surface is nested.

2. `set-interval <key> <minutes>` → `update <key> [flags]`. The PATCH
   endpoint already supported watch_interval / is_active / reason /
   instruction, but the CLI only exposed watch_interval. `update` now
   covers all four via flags, with at-least-one-flag enforced. `pause` /
   `resume` shortcuts are kept since they're the highest-frequency ops
   and `update --active=false` reads worse for them.

Helper `build_update_body` is extracted as a pure function so the empty-
flag, partial-flag, and interval-validation paths are unit-testable
without an HTTP client.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Collaborator

@qin-ctx qin-ctx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the REST watch_interval validation issue. I also left non-blocking comments for stale CLI wording after the command moved under ov task watch.


model_config = ConfigDict(extra="forbid")

watch_interval: Optional[float] = None
Copy link
Copy Markdown
Collaborator

@qin-ctx qin-ctx May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] (blocking) UpdateWatchRequest.watch_interval accepts any float, so PATCH /api/v1/watches/{task_id} can persist watch_interval <= 0 through the public REST management API. For example, sending {"watch_interval": -1} to an active task makes WatchManager.update_task deactivate it and store the negative interval. A later resume with {"is_active": true} then raises ValueError("watch_interval must be > 0 for active tasks"), which this router currently maps to 404. CLI and MCP already reject non-positive intervals, so REST should match that contract. Please validate this field at the request boundary, for example with Field(gt=0) or a model validator, and return a real invalid-argument response with regression tests for 0 and negative values.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch and fully valid — REST was the odd one out. Fixed by adding a field_validator on UpdateWatchRequest.watch_interval that rejects non-positive values at the request boundary (None still means "leave unchanged"). The validator's docstring spells out the original failure mode (deactivate-store-then-resume-blows-up) so the rationale survives.

Regression test added covering -1, 0, and -42.5 — all assert 422 now. The three control planes (REST/CLI/MCP) are now uniform on "non-positive is invalid at the boundary".

Comment thread crates/ov_cli/src/commands/watch.rs Outdated
fn validate_interval_minutes(minutes: f64) -> Result<()> {
if !(minutes > 0.0) {
return Err(Error::Parse(format!(
"minutes must be > 0 (got {minutes}). To pause a watch task, use `ov watch pause`."
Copy link
Copy Markdown
Collaborator

@qin-ctx qin-ctx May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] (non-blocking) This user-facing error still points to the old ov watch pause command after the CLI moved under ov task watch. The nearby comment also still mentions ov watch set-interval. Please update these strings to ov task watch pause and the current ov task watch update --interval ... flow so users do not follow a command path that no longer parses.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — stale after the CLI nesting refactor. Updated both:

  • The error string in validate_interval_minutes now points to ov task watch pause.
  • The helper's docstring now references ov task watch update (replacing the obsolete ov watch set-interval).

Comment thread openviking/server/mcp_endpoint.py Outdated
# MCP exposes the minimum closure: list + cancel. Pause/resume/trigger/
# set-interval are intentionally NOT exposed — they're either low-value for
# agents or invite unwanted autonomous decisions. Power users should reach
# for the REST API or `ov watch` CLI for those operations.
Copy link
Copy Markdown
Collaborator

@qin-ctx qin-ctx May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] (non-blocking) This comment still points power users to ov watch, but the PR moved the command under ov task watch. The same block also mentions the removed set-interval verb. Please update this to the current CLI surface, for example ov task watch and update --interval, so the MCP-side guidance stays aligned with the shipped command names.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refreshed the comment block. The new wording mentions ov task watch * (CLI surface), explicitly lists pause/resume/trigger/update --interval as the actions that intentionally aren't on MCP, and drops the removed set-interval reference.

REST router (`watches.py`):
- UpdateWatchRequest: add field_validator rejecting watch_interval <= 0 at
  the request boundary. Previously REST accepted any float and forwarded
  to WatchManager.update_task, which deactivated the task and stored the
  bad cadence. A later resume (`is_active=true`) then failed inside
  update_task with ValueError that this router maps to 404 — misleading
  callers about the root cause. Now matches the CLI/MCP boundary checks
  so all three control planes agree on "non-positive is invalid". Test
  added covering 0, -1, and -42.5.

CLI (`commands/watch.rs`):
- Update the pause-hint error string from `ov watch pause` to
  `ov task watch pause` and update the helper docstring to reference
  `ov task watch update --interval` after the CLI nesting refactor.

MCP (`mcp_endpoint.py`):
- Refresh the watch-management section comment: mention `ov task watch *`
  (not `ov watch`) and `update --interval` (not the removed `set-interval`
  verb), keeping MCP-side guidance aligned with the shipped command names.

Addresses: 3 review comments from @qin-ctx on PR volcengine#2110 (1 blocking, 2
non-blocking).
@qin-ctx qin-ctx merged commit 74414c9 into volcengine:main May 19, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in OpenViking project May 19, 2026
r266-tech added a commit to r266-tech/OpenViking that referenced this pull request May 19, 2026
r266-tech added a commit to r266-tech/OpenViking that referenced this pull request May 19, 2026
r266-tech added a commit to r266-tech/OpenViking that referenced this pull request May 19, 2026
r266-tech added a commit to r266-tech/OpenViking that referenced this pull request May 19, 2026
qin-ctx pushed a commit that referenced this pull request May 20, 2026
…table (#2110) (#2134)

* docs(mcp): document list_watches and cancel_watch in MCP integration table (#2110)

* docs(mcp): mirror Watch tools table update to zh guide (#2110)
qin-ctx pushed a commit that referenced this pull request May 20, 2026
* docs(api): document Watch Management endpoints from #2110

* docs(api): document Watch Management endpoints from #2110 (ZH)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants