ts: mesh.jobs cancel/status/wait facades (TS parity for #1074) - #1079
Conversation
TypeScript parity for the Python facades shipped in #1077. Three new module-level async functions on mesh.jobs mirror the existing postEvent / subscribeEvents DDDI-clean shape — callers with only a jobId no longer need to construct a JobProxy or pass MCP_MESH_REGISTRY_URL explicitly: async cancel(jobId: string, reason?: string): Promise<void> async status(jobId: string): Promise<JobStatus> async wait(jobId: string, timeoutSecs?: number): Promise<unknown> Each: resolveRegistryUrl() → _getOrCreateProxy() (LRU-cached) → napi dispatch → translateJobError() for typed exception re-classification. Signatures verified against JsJobProxy in src/runtime/core/src/jobs_napi.rs; underlying napi binding already exposes cancel/status/wait so no Rust changes were needed. JobStatus interface added alongside JobEvent / JobEventReceipt, exported from mesh.jobs. Fields mirror job_to_json in jobs_napi.rs: id, capability, owner_instance_id, status, progress, progress_message, result, error, submitted_payload, attempt_count, max_retries, max_duration, total_deadline, lease_expires_at, last_heartbeat_at, submitted_at, submitted_by. Only id and status are required. MeshJobsNamespace in src/runtime/typescript/src/index.ts extended with cancel/status/wait alongside postEvent/subscribeEvents. Public exports added. Bonus generalization: resolveRegistryUrl()'s error message prefix generalized from "mesh.jobs.postEvent:" → "mesh.jobs:". Now accurate for all four facades (mirrors the same generalization the Python BLOCKER fix made in #1077). Tests (jobs.spec.ts): 23 → 38 (+15 new). For each new facade: happy path, MCP_MESH_REGISTRY_URL missing, JobNotFoundError translation, JobTerminalError translation (cancel/wait). Plus one shared-cache test asserting cancel → status → wait reuse one cached proxy. Wait-timeout: napi maps JobError::Timeout to message "timeout: wait timed out after ...". translateJobError doesn't dispatch on this; the facade re-raises as plain Error. Documented in the wait JSDoc; a proper TimeoutError class with substring dispatch is a follow-up if usage warrants. Docs: docs/concepts/jobs.md "Lifecycle facades by jobId" section now has Python | TypeScript tabs. src/core/cli/man/content/jobs_typescript.md mirrors the Python variant's facade documentation. Out of scope (separate work): - Java parity (MeshJobs.cancel/status/wait) — separate PR per the polyglot cadence (mirrors #1041 → #1043 → #1045 trilogy) - TimeoutError class + dispatch — follow-up if needed Closes #1078 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds three new module-level async facades ( ChangesTypeScript Job Lifecycle Facades
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/typescript/src/jobs.ts`:
- Line 84: The JobStatus.status union in jobs.ts currently omits "pending",
causing incorrect exhaustiveness and runtime assumptions; update the type
declaration for JobStatus.status to include "pending" (i.e., add "pending" to
the union alongside "working" | "input_required" | "completed" | "failed" |
"cancelled"), then scan for and update any switch statements, exhaustive
type-narrowing, or handling logic that assumes the old set (look for usages of
JobStatus.status, JobStatus, and any switch/case or exhaustive checks) to
account for the new "pending" state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 524bed91-8b09-43be-825d-b052cedd36b1
📒 Files selected for processing (5)
docs/concepts/jobs.mdsrc/core/cli/man/content/jobs_typescript.mdsrc/runtime/typescript/src/__tests__/jobs.spec.tssrc/runtime/typescript/src/index.tssrc/runtime/typescript/src/jobs.ts
| /** Instance id of the replica currently holding the lease (if any). */ | ||
| owner_instance_id: string | null; | ||
| /** Lifecycle status. */ | ||
| status: "working" | "input_required" | "completed" | "failed" | "cancelled"; |
There was a problem hiding this comment.
Add "pending" to the JobStatus.status union.
Line [84] excludes "pending", but this surface is documented as the full job-row snapshot. A submitted job can be observable in pending state before claim, so the current type can mislead exhaustive handling.
Proposed fix
- status: "working" | "input_required" | "completed" | "failed" | "cancelled";
+ status:
+ | "pending"
+ | "working"
+ | "input_required"
+ | "completed"
+ | "failed"
+ | "cancelled";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| status: "working" | "input_required" | "completed" | "failed" | "cancelled"; | |
| status: | |
| | "pending" | |
| | "working" | |
| | "input_required" | |
| | "completed" | |
| | "failed" | |
| | "cancelled"; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/typescript/src/jobs.ts` at line 84, The JobStatus.status union in
jobs.ts currently omits "pending", causing incorrect exhaustiveness and runtime
assumptions; update the type declaration for JobStatus.status to include
"pending" (i.e., add "pending" to the union alongside "working" |
"input_required" | "completed" | "failed" | "cancelled"), then scan for and
update any switch statements, exhaustive type-narrowing, or handling logic that
assumes the old set (look for usages of JobStatus.status, JobStatus, and any
switch/case or exhaustive checks) to account for the new "pending" state.
Java parity for the Python facades shipped in #1077 and TypeScript facades in #1079. Three new static methods (with overloads) on MeshJobs mirror the existing postEvent / subscribeEvents DDDI-clean shape — callers with only a jobId no longer need to construct a JobProxy(jobId, registryUrl) and pass MCP_MESH_REGISTRY_URL explicitly: public static void cancel(String jobId, String reason) public static void cancel(String jobId) public static Map<String, Object> status(String jobId) public static Object await(String jobId, double timeoutSecs) public static Object await(String jobId) Each: resolveRegistryUrl() → getOrCreateProxy() (LRU-cached) → dispatch to the existing JobProxy instance method → substring-match translation to typed exceptions (JobNotFoundException, JobTerminalException) via a new package-private translateJobError helper. Java naming nuance: 'await' (not 'wait') because Object.wait() is final. Matches the existing JobProxy.await() instance method precedent. What was already in place (no Rust/JNR/JobProxy changes needed): - FFI: mesh_job_proxy_cancel/status/wait in jobs_ffi.rs - JNR: declarations in MeshCore.java:648-670 - JobProxy.cancel/status/await instance methods - JobNotFoundException, JobTerminalException, MeshException classes - LRU proxy cache infrastructure Bonus generalization: resolveRegistryUrl's error message prefix generalized from 'MeshJobs.postEvent:' → 'MeshJobs:'. Now accurate for all four facades. Mirrors the same generalization the Python BLOCKER fix made in #1077 and the TS bonus in #1079 — the trilogy is now consistent across all three runtimes in this respect. Tests (MeshJobsTest.java): 14 → 27 (+13 new). For each new facade: arg validation (null/empty jobId), env resolution (MCP_MESH_REGISTRY_URL missing), cache hit reuse. Plus the cross-facade shared-cache test asserting cancel/status/await reuse one cached proxy (mirrors Python W6 from #1077's review fixes). await timeout: JobProxy.await contract says timeoutSecs <= 0.0 or non-finite means 'no timeout'. The MeshJobs.await(jobId) no-arg overload delegates to await(jobId, -1.0). Docs: docs/concepts/jobs.md 'Lifecycle facades by jobId' section now has Python | TypeScript | Java tabs. src/core/cli/man/content/jobs_java.md mirrors the Python/TS variants' facade documentation. Out of scope (separate work): - Strongly-typed JobStatus record — returns Map<String, Object> matching the JobProxy.status() shape; a record with typed fields is a follow-up if usage warrants - Caller authorization on lifecycle ops — design discussion at #1076 Closes #1080 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…1081) ## Summary Java parity for the `mesh.jobs.cancel/status/wait` facades shipped in Python (#1077) and TypeScript (#1079). Three new static methods (with overloads) on `MeshJobs` mirror the existing `postEvent` / `subscribeEvents` DDDI-clean pattern — callers with only a `jobId` no longer need to construct `new JobProxy(jobId, registryUrl)` and pass `MCP_MESH_REGISTRY_URL` explicitly. ```java public static void cancel(String jobId, String reason) throws MeshException public static void cancel(String jobId) throws MeshException public static Map<String, Object> status(String jobId) throws MeshException public static Object await(String jobId, double timeoutSecs) throws MeshException public static Object await(String jobId) throws MeshException ``` Each: `resolveRegistryUrl()` → `getOrCreateProxy()` (LRU-cached) → dispatch to the existing `JobProxy` instance method → substring-match translation to typed exceptions via a new package-private `translateJobError` helper. **Java naming nuance**: `await` (not `wait`) to avoid readability confusion with the inherited `Object.wait()` / `wait(long)` / `wait(long, int)` overload family, and to match the existing `JobProxy.await(double)` precedent. ## What was already in place (no work needed) - FFI: `mesh_job_proxy_cancel/status/wait` in `jobs_ffi.rs` - JNR: declarations in `MeshCore.java:648-670` - `JobProxy.cancel/status/await` instance methods - `JobNotFoundException`, `JobTerminalException`, `MeshException` classes - LRU proxy cache infrastructure ## Bonus generalization `resolveRegistryUrl`'s error message prefix generalized from `"MeshJobs.postEvent:"` → `"MeshJobs:"`. Now accurate for all four facades. Mirrors the same generalization the Python BLOCKER fix made in #1077 and the TS bonus in #1079 — the trilogy is now consistent across all three runtimes in this respect. ## Trilogy complete - Python facades (#1077) ✓ merged - TypeScript facades (#1079) ✓ merged - **Java facades (this PR)** ← closes the trilogy ## Out of scope - Strongly-typed `JobStatus` record — returns `Map<String, Object>` matching the existing `JobProxy.status()` shape. A record with typed fields is a follow-up if usage warrants. - Caller authorization on lifecycle ops — design discussion at **#1076**. ## Review Notes Independent review: 0 BLOCKER, 1 WARNING (addressed), 4 INFOs (3 addressed, 1 skipped as docs cosmetic). - **W1 — Triplicated try/catch boilerplate fixed**: each facade's translation block collapsed from 6 lines to a one-liner `throw translateJobError(exc);`. The previous indirection (`if (translated != exc) throw translated; throw exc;`) was dead-equivalent on Java because `translateJobError` chains the original exception as `cause` via the typed constructor. Net -12 lines on the facade region. - **I1 — `await` naming Javadoc accuracy**: the previous text claimed "overloading would be a compile error" (wrong — `wait(double)` is overloading, legal). Updated wording across 3 surfaces (`MeshJobs.java`, `JobProxy.java`, `jobs_java.md`) to cite the real reason: readability confusion with the inherited `Object.wait()` overload family + the existing `JobProxy.await(double)` precedent. - **I2 — Test message symmetry**: `JobTerminalException` translation test now asserts message preservation matching the `JobNotFoundException` test. - **I3 — `@DisabledIfEnvironmentVariable`**: the three `*_failsCleanlyWhenRegistryUrlUnset` tests now use JUnit 5's declarative annotation instead of silent `return` — skips are visible in test reports, no green-test illusion in CI shells that export `MCP_MESH_REGISTRY_URL`. I4 (4-column docs table) skipped as cosmetic. Closes #1080 ## Test plan - [x] `mvn install -pl mcp-mesh-core,mcp-mesh-sdk -am`: BUILD SUCCESS - [x] `mvn test -pl mcp-mesh-sdk`: 98 tests pass (`MeshJobsTest` 27/27) - [x] `@DisabledIfEnvironmentVariable` verified visible in report when env set - [x] `tsuite uc23_meshjob_java --parallel 4`: 27/27 - [x] `mkdocs build` clean - [x] Cross-runtime scope: no edits to Python / TS / Rust / FFI / JNR / `JobProxy.cancel/status/await` instance methods / Python `jobs.md` / TS `jobs_typescript.md` - [x] Public-artifact framing: no naming downstream consumers; structural language throughout <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added convenience static methods to manage job lifecycle—cancel, check status, and await completion—using only a job ID, without requiring a JobProxy instance. * **Documentation** * Enhanced job lifecycle documentation with comprehensive examples, detailed timeout semantics, and cross-runtime operation guidance. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/dhyansraj/mcp-mesh/pull/1081?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary **v2.3.0 — Lifecycle facades across the polyglot trilogy + unified dependency-injection contract.** v2.2 introduced the MeshJob substrate. v2.3 completes the lifecycle surface so callers that hold only a `job_id` can drive `cancel` / `status` / `wait` through DDDI-clean module-level facades — the same shape `post_event` and `subscribe_events` already had. The DI rules for `McpMeshTool` and `MeshJob` parameters are unified under a single positional contract, eliminating a silent wrong-proxy footgun when both types appeared in the same tool. ## What ships ### Lifecycle facades by `job_id` (Python #1077, TS #1079, Java #1081) | Operation | Python | TypeScript | Java | | ------------------------ | -------------------------------------------------- | ------------------------------------------------ | --------------------------------------------- | | Cancel a running job | `await mesh.jobs.cancel(job_id, reason=None)` | `await mesh.jobs.cancel(jobId, reason?)` | `MeshJobs.cancel(jobId[, reason])` | | Read latest job state | `await mesh.jobs.status(job_id)` | `await mesh.jobs.status(jobId)` | `MeshJobs.status(jobId)` | | Wait for terminal state | `await mesh.jobs.wait(job_id, timeout_secs=None)` | `await mesh.jobs.wait(jobId, timeoutSecs?)` | `MeshJobs.await(jobId[, timeoutSecs])` | Underlying `JobProxy.cancel/status/wait` was already shipped in v2.2; this adds the module-level wrappers that resolve the registry URL internally — no more `JobProxy(jobId, registryUrl)` plumbing in user code. Typed errors (`JobNotFoundError` / `JobTerminalError`) translate consistently across all three runtimes. TS adds a typed `JobStatus` interface exported from `mesh.jobs`. ### Unified positional dependency injection (#1082, Python) `McpMeshTool` and `MeshJob` parameters now share a **single positional `dep_index` namespace** in parameter declaration order. Each `dependencies[i]` strictly pairs with one parameter position; the slot's type determines what gets constructed. Previously, the two types had inconsistent injection rules (positional for `McpMeshTool`, by-name for `MeshJob`), producing wrong-proxy injection when both appeared in the same tool with the `MeshJob` capability listed first in `dependencies[]`. **Behavior change to call out**: users who deliberately wrote `MeshJob` params out-of-order with their `dependencies[]` array (relying on the previous by-name resolution) now need to put params in the same order as deps. The natural same-order case continues to work unchanged. TypeScript and Java SDK DI paths still follow the orthogonal injection contract — their port to the unified positional rule is tracked separately. ### `health_check_ttl` refresh on the user loop (#1073) `@mesh.agent(health_check=fn, health_check_ttl=N)` now actually refreshes every N seconds. Previously, the result was stored exactly once at startup and served forever — a failed check during startup cached as unhealthy and permanently failed k8s readiness probes. The refresh loop runs on the user loop (same loop as `lifespan` and tools) so health checks touching loop-bound resources work without cross-loop errors. A lifespan-ready signal gates the refresh start so iterations don't fire during user `__aenter__`. ### FastMCP lifespan documentation correction (#1073) The v2.2.4 "Loop topology" docs incorrectly showed a FastAPI-style `app.state.pool` example — FastMCP's lifespan receives a server instance, not a FastAPI app. Three doc surfaces rewritten to the canonical module-level globals pattern. ## Mechanical bundle - `scripts/bump_version.py 2.2.4 → 2.3.0` (419 files updated across 36 categories) - `helm dependency update helm/mcp-mesh-core` - `cargo generate-lockfile` (src/runtime/core) - `RELEASE_NOTES.md` — new v2.3.0 entry with full narrative ## Post-merge Per the established v2.2.3 / v2.2.4 publish pattern: 1. `/dev` reset to pull the merged main 2. `gh release create v2.3.0 --target main --title "v2.3.0" --notes-file <v2.3.0 section> --latest` (one-shot) 3. Wait for all 4 publish jobs to land (npm × 7 packages, PyPI, Maven Central, crates.io) 4. Verify `npm view @mcpmesh/core version` returns `2.3.0` ## Test plan - [x] `mvn install` BUILD SUCCESS for Java - [x] Python unit suite: 1010/1010 - [x] uc02_agent_lifecycle: 23/23 - [x] uc21_meshjob (Python): 21/21 - [x] uc22_meshjob_ts: 24/24 - [x] uc23_meshjob_java: 27/27 - [x] `mkdocs build` clean - [ ] Release workflow fires cleanly across all 4 registries Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
TypeScript parity for the Python
mesh.jobs.cancel/status/waitfacades shipped in #1077. Three new module-level async functions mirror the existingpostEvent/subscribeEventsDDDI-clean pattern — callers with only ajobIdno longer need to constructnew JobProxy(jobId, registryUrl)and passMCP_MESH_REGISTRY_URLexplicitly.Each:
resolveRegistryUrl()→_getOrCreateProxy()(LRU-cached) → napi dispatch →translateJobError()for typed exception re-classification. Underlying napi binding (JsJobProxy.cancel/status/wait) was already exposed — no Rust changes.JobStatusinterface added alongsideJobEvent/JobEventReceipt, exported frommesh.jobs. Fields mirrorjob_to_jsoninjobs_napi.rs: required fields typedT,Option<T>fields emitted asT | null(every key always present; only null-checks needed, no key-presence checks).MeshJobsNamespaceextended withcancel/status/waitalphabetically; public exports added.Bonus generalization
resolveRegistryUrl's error message prefix generalized from"mesh.jobs.postEvent:"→"mesh.jobs:". Now accurate for all four facades — mirrors the same generalization the Python BLOCKER fix made in #1077.Out of scope
TimeoutErrortyped class —wait()currently rejects with plainErrorwhose message starts with"timeout:". Substring contract pinned by test. Follow-up if needed.Review Notes
Independent review: 0 BLOCKER, 1 WARNING (addressed in amended commit), 5 INFOs (skipped as cosmetic).
JobStatusinterface usedfield?: Tfor nullable fields, butjob_to_jsonalways emits every field withValue::Nullfallback. Tightened tofield: T | nullwhere appropriate, andfield: T(no optional, no null) for fields the Rust binding emits unconditionally. Added one test pinning the every-key-present contract viaObject.keyscomparison.INFOs skipped:
Promise<unknown>vsAny(the stricter shape is correct), repeated try/catch boilerplate matchespostEvent's style, emptytoHaveBeenCalledWith()matcher is intentional for nullary calls, JSDoc null-vs-undefined nuance, doc table layout.Test plan
npm run buildclean (no TS compile errors after type tightening)import { cancel as cancelFacade }); SDK exports stay canonical —import { cancel } from "@mcpmesh/sdk"works unaliased for end usersjobs_java.md/ Pythonjobs.mdvariantCloses #1078
Summary by CodeRabbit
Release Notes
New Features
Tests