Repository navigation
Add standalone Manus glove calibration worker and browser example - #1406
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
Stage 1 of Manus glove calibration. - src/plugins/manus/calibration: manus_glove_calibrator, a separate executable in the Manus plugin package (not a registered tracking plugin). It owns the SDK connection for one attempt, runs the SDK's calibration steps on operator confirmation, exports each glove's calibration, shuts the SDK down, then sends one terminal result over a line-delimited JSON socket. The wire format and step engine are an SDK-free static library with a fake-SDK test suite. - examples/manus_calibration: a plain HTML page served by a standard library HTTP and server-sent-events server. One browser owns an attempt, confirmations name the prompt they answer, and Host, Origin and a per-tab client id are checked. A supervisor bounds answers only the worker can give, terminates a silent worker with an uncertain outcome, and refuses new attempts while an unreaped worker may hold the gloves. Exported files are verified and published through a hard link and an atomic manifest only after a completed result and clean exit. - CMake: build and install the worker with the manus component; add its tests when the SDK is present. Negative SDK step times (continuous steps with an estimated duration) are operator-paced and carry the estimate as guidance; the worker claims no completion signal for them. A failed SDK release downgrades a completed or cancelled outcome to failed. Known issue: with the real SDK and no working license, the SDK call that starts a step did not return within the supervisor's bound; the cause is not established. An end-to-end calibration, export and tracking load are not yet demonstrated, and the final revision was not retested. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… lint CI rejects unformatted C++; six worker sources were not clang-format 14 clean. Also explain the two intentionally empty exception handlers in the page server, and make the discarded values in the example's tests explicit. No behavior change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Silences the remaining code-quality finding; no behavior change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
- Parser: check the type of a worker line before the lookup so an unhashable JSON value is a protocol error, not a TypeError. - Worker: ignore the rest of a read once the attempt has finished, so no rejection can follow the terminal result. - Engine: report a tiny nonzero SDK time as one millisecond on the wire instead of 0.000, which the supervisor rejects. - Tests: prefix the Catch2 cases and map their tags to CTest labels, and tag them [unit] so `ctest -L unit` selects them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
A failed SDK calibration stop or shutdown can follow any outcome, and the process then exits while the gloves may still be in use. The terminal result now always carries `released`; the supervisor treats a result with released=false as uncertain, publishes nothing, and refuses new attempts until the program restarts. Process exit alone is not evidence of release. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Drop the store (private staging, size/SHA-256 re-verification, hard-links to unique names, JSON manifests, list_published). The worker already writes each export atomically under its final <attempt>_<side>.mcal name and never overwrites, so the supervisor now hands it the real output folder and reports the files it wrote. A cancelled, failed or uncertain attempt removes whatever it wrote. The example page gets a download link for each generated file; the server serves only the files the last completed calibration reported. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
bf0e178 to
b9f3a08
Compare
AndreusNvidius
left a comment
There was a problem hiding this comment.
🤖 Agent-generated (Codex · gpt-6)
Summary
Reviewed b9f3a087 against the stated Stage 1 objective, repository guidance, and separate readability, conciseness, and correctness assessments. The SDK-free engine, process supervisor, and presentation/controller boundaries are readable and well scoped. Two reproducible Python behavior defects and a test-integration gap need attention. Full calibration and loading the resulting MCAL files remain unverified, as the PR already discloses.
Legend: 🚫 Blocker · 💡 Suggestion · 🔍 Nit
| Finding | |
|---|---|
| 🚫 | Resolve the validated executable path before launch. examples/manus_calibration/python/isaaccapture_examples/manus_calibration/worker.py:102 — --worker ./manus_glove_calibrator passes the CLI's file/executable checks, but Path strips ./, and create_subprocess_exec then searches PATH for the bare name. I reproduced failed / spawn_failed / No such file or directory with a valid executable in the current directory. If PATH contains a different binary with that name, it can launch that binary instead. Resolve the file to an absolute path before storing/launching it. |
| 💡 | Preserve cancellation before the start command. examples/manus_calibration/python/isaaccapture_examples/manus_calibration/worker.py:439 — Cancelling while waiting for ready, before start is sent, terminates the worker. The C++ worker handles SIGTERM in its idle state by emitting result(attempt=null, outcome=cancelled, released=true). This guard rejects that legitimate reply; _decide then prioritizes the violation over the user cancellation. A scripted worker reproducing the C++ response yielded FAILED: worker broke the protocol (Result before the attempt was started). Accept this narrowly defined terminal response during pre-start cancellation while retaining rejection of unexpected pre-start events. |
| 🚫 | Register the Python suite in the repository test tree. examples/manus_calibration/pyproject.toml:31 — The new suite lives under examples/manus_calibration/tests/ and has no CTest registration. tests/AGENTS.md explicitly requires new suites under tests/python/examples/<example>/ and wiring through CMake. The standard build jobs run CTest, so these protocol, ownership, watchdog, and HTTP regressions are currently outside that CI path. |
| 💡 | Consider dropping checksum metadata without a consumer. src/plugins/manus/calibration/core/engine.cpp:428 — The custom SHA-256 implementation computes a value repeated in exported and result; the supervisor compares those reported values but does not hash the saved file, and the page/download representation omits the checksum. Removing this currently unused metadata would reduce implementation and protocol surface. Optional simplification only; the documented design deliberately has no file re-verification step. |
Validation
Ran the Python suite on Python 3.13: 265 passed, 116 failed. The HTTP tests were blocked by this environment's PermissionError binding 127.0.0.1; this does not establish HTTP regressions. Separately reproduced both behavior defects above. C++ builds/tests and real glove calibration were not run here. Checked existing PR discussion; the findings above do not repeat the already addressed parser, timing-precision, terminal-message, or CTest-label comments.
Actionables (for bots — copy-paste-ready for AI)
Agent-generated suggestions, not human-vetted obligations. Skip anything wrong, already addressed, or not worth the churn.
examples/manus_calibration/python/isaaccapture_examples/manus_calibration/worker.py:102— Store an absolute executable path; add coverage for a valid./workerabsent from PATH and for a same-named PATH executable.examples/manus_calibration/python/isaaccapture_examples/manus_calibration/worker.py:439— Recognize the idle worker's clean terminal cancellation response when cancellation precededstart; add a regression exercising the SIGTERM/result exchange.examples/manus_calibration/pyproject.toml:31— Move the suite totests/python/examples/manus_calibration/, use the repository path helper for imports, and register it with CTest without introducing SDK/GPU requirements.src/plugins/manus/calibration/core/engine.cpp:428— Optionally remove unused SHA-256 computation and matching protocol fields/validation/tests if no checksum consumer is intended.
Spawn the worker by absolute path so a bare ./name is not re-read as a PATH lookup, accept an idle worker's clean cancelled result when the user cancelled before start, and move the Python suite into the repository test tree with CTest registration. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
NVdejung
left a comment
There was a problem hiding this comment.
🤖 Automated first-pass review (Claude, for Dejun) — I'll follow up personally where needed.
Summary — Adds manus_glove_calibrator (an SDK-free step engine and wire format plus a thin Manus SDK binding) and a stdlib-only browser page that supervises it and serves the resulting MCAL files. Supervision, ownership and file cleanup are carefully built and widely tested against fakes. One blocker from the earlier review is still open, and the continuous-step pacing needs a hardware check before anyone relies on the files. Neither the scan nor my own read found anything internal (hosts, paths, names), which matters because the repo is public.
Must fix
worker.py:102— Still open from the earlier review: a relative--workersuch as./manus_glove_calibratorpasses_validate, but it is then launched through aPATHsearch. Make the path absolute once, either here or at__main__.py:74, and add a relative-path test. The fixture only ever passes absolute paths.
Suggestions
engine.cpp:344,app.js:81— Please verify on real gloves. For a continuous step the page tells the operator to move first and then confirm. SoStartStepruns after the movement, and the next prompt replaces the instruction at once. On a glove's last step,finish()(engine.cpp:392) followsstart_step()immediately. IfStartStepis asynchronous, that step gets no sampling time; the test modelled on the real gloves ends with a 3 s step. IfStartStepinstead blocks until it sees movement, that could explain the 30 s hang in the Known issue. Before blaming the SDK or the licence, try "confirm, then move for about the estimate", i.e. pace it like a motion step.test_engine.cpp:273pins the current order.worker.py:504—_on_rejectedonly handlesconfirm. Suppose--out-direxists but isn't writable:mkdir(exist_ok=True)passes and the worker rejectsstart(engine.cpp:108). The page then shows "Starting…" until the startup bound expires (60 s by default) and ends with "Outcome uncertain", never showing the worker's reason. Fail the attempt with the worker's message whenstartis rejected, and checkos.access(out_dir, W_OK | X_OK)before launching. The fake worker never rejectsstart, so no test covers this.src/plugins/manus/CMakeLists.txt:109—calibration/is added after the SDK gate'sreturn()(line 82). Somanus_calibration_coreand its 119 Catch2 cases (tests/cpp/plugins/CMakeLists.txt:21) never build in CI, which has no Manus SDK. Addcalibration/corebefore the gate and keepappafter it. Make the core Linux-only, because it uses POSIX calls and there is a Windows workflow. No workflow runs the pytest suite either; this is probably the test-integration gap the earlier review mentioned.worker.py:439— The earlier review's pre-start-cancel finding is also still open, and the tests can't catch it. On SIGTERM beforestart, the real worker sends aresultwithattempt: null(app/main.cpp:243→engine.cpp:475), but the fake just dies (fake_worker.py:97). Make the fake behave like the worker.__main__.py:87— The page handles only SIGINT and SIGTERM, and the worker has no SIGHUP handler either (app/main.cpp:214). If the SSH session the README recommends drops, both processes likely die on SIGHUP. A first glove's<attempt>_left.mcalthen stays behind, contrary to example README:92. Handle SIGHUP like SIGTERM on both sides. Better still, haveterminate()unlinkm_exportedwhen the outcome is not completed, so the guarantee doesn't depend on the page process surviving.protocol.py:384— The parser treats an emptyinstructionas a protocol violation: the worker is killed and the outcome is uncertain. But the worker forwards the SDK description verbatim (manus_sdk.cpp:225), and the contract doesn't forbid an empty one. Either reject it up front in the step check (engine.cpp:258) or accept it, and document which.server.py:356—tab.changed.clear()runs afterawait writer.drain(). A change that arrives whiledrain()yields (e.g. over a slow tunnel) is lost until the next change or the 15 s keepalive. Clear the event before taking the snapshot.- Stale after the last commit:
export_writer.hpp:29says the file is renamed into place, but the code useslink().sha256.hpp:13andprotocol.py:87say the supervisor verifies the files, but nothing hashes them any more.protocol.py:253still says "staging directory".staging_diritself (worker README:46) is new in this PR. Renaming it toout_dircosts nothing now, but becomes a compatibility constraint once this PR and#1408merge.
- Nits:
ready.sdk_versionis always null becausereadyis sent beforeinitialize()(app/main.cpp:235,manus_sdk.cpp:119).link()(export_writer.cpp:84) fails on vfat/exFAT. An--out-diron a USB stick therefore loses the calibration only at export time. Probe for it indirectory_usableor note it in the README.
Conflicts
- With
main: none in the local 3-way merge. The branch is 2 commits behind; no rebase needed. - With open PRs:
#1408(same author, stacked on this branch) shares 12 example files with no textual conflict. Carry the fixes above (and anystaging_dirrename) into it, and rebase it once this merges.#1153(huadongzhang97),#1250(jmex-sw),#1252(obzhimin, draft) and#1407(HAOTianGa03) each append a block to the end oftests/cpp/plugins/CMakeLists.txt. That is a one-hunk textual conflict with no semantic overlap; whoever merges second keeps both blocks.#998,#1321,#1324and#1340touch the same file but target other bases, so conflicts were not computed.
Change-by-change (46 files)
| File / area | What changed | Makes sense? |
|---|---|---|
src/plugins/manus/CMakeLists.txt, calibration/CMakeLists.txt, calibration/{core,app}/CMakeLists.txt |
Adds SDK-free manus_calibration_core and the manus_glove_calibrator executable (a stub target when the SDK is missing), installed beside manus_hand_plugin |
|
calibration/core/protocol.*, json.* |
Strict NDJSON command parser (flat objects, integers only, no duplicate keys, 64 KiB cap) and event serializer with UTF-8 repair | ✅ |
calibration/core/engine.* |
Single-threaded step machine: discovery and glove-ID pinning, per-step prompts and tokens, sampling window for motion steps, per-glove export, one terminal result with released |
|
calibration/core/export_writer.* |
.part plus fsync, link() to the final name (never overwrites), directory fsync |
|
calibration/core/sha256.* |
Self-contained SHA-256 for the reported hashes | ✅ (comment is stale) |
calibration/app/main.cpp |
CLI; SIGTERM, SIGINT and parent death all trigger cancel; socket line framing; 50 ms poll loop | ✅ (no SIGHUP handler; sdk_version always null) |
calibration/app/manus_sdk.* |
Manus Integrated SDK binding: init, host connect, landscape callback, calibration calls, export, shutdown | ✅ |
calibration/README.md |
Worker process contract and wire format | staging_dir as a "historical" name on a brand-new protocol; empty instruction not covered |
tests/cpp/plugins/CMakeLists.txt, tests/cpp/plugins/manus_calibration/* |
Fake SDK and 119 Catch2 cases for the engine, protocol, writer and SHA-256 | ✅ thorough; |
examples/.../worker.py |
asyncio supervisor: socketpair spawn, strict event-order checks, inactivity deadlines, TERM then KILL escalation, outcome decision, removal of files from non-completed attempts | --worker path; rejected start ignored; pre-start cancel |
examples/.../protocol.py |
Python mirror of the wire format | instruction |
examples/.../controller.py, wire.py |
Single-attempt view and ownership state; per-tab JSON snapshot (token sent only to the owner) | ✅ |
examples/.../server.py |
Stdlib HTTP plus SSE server, Host and Origin checks, X-Client-Id on actions, downloads limited to the last completed files |
✅ (minor SSE wake race) |
examples/.../__main__.py, __init__.py, pyproject.toml |
CLI and packaging (stdlib only, Python 3.11 to 3.13) | --worker not made absolute; SIGHUP not handled |
examples/.../static/* |
The page: text-only rendering, CSP-compatible, progress bar, download links | ✅ (order of the continuous-step guidance needs verifying) |
examples/manus_calibration/README.md |
Usage, safety notes, status and known gaps | ✅ (the line 92 guarantee has a SIGHUP gap) |
examples/manus_calibration/tests/* |
Scripted fake worker and about 190 pytest functions (HTTP, SSE, watchdog, protocol, controller, worker) | ✅; start and dies silently on SIGTERM |
I read all source and doc files in full. I only skimmed the roughly 7.5k lines of tests, via their case lists plus the sections cited above.
Reviewed at b9f3a08 · 46 files · 2026-11-05 14:54 PST
|
Addressed the review findings in 4a283b6:
🤖 Generated with Claude Code |
End an attempt with the worker's reason when it rejects start, and refuse an unwritable output folder before launching. Clear the SSE wake event before the snapshot so a concurrent change is not lost. Correct stale comments about renaming and hash verification, and note the hard-link and sdk_version behavior. The fake worker now rejects start and reports an idle cancel like the real worker. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
|
Addressed the second review's low-risk findings in 8573854 and 31cfb3d:
Still open and being discussed separately: the 🤖 Generated with Claude Code |
Add calibration/core before the SDK gate so its Catch2 suite is built in CI, which has no Manus SDK, on Linux. The SDK-linked worker stays after the gate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…ename staging_dir to out_dir A dropped terminal session now cancels the worker and the page instead of killing them mid-export. The worker forwards the SDK step description verbatim, so an empty instruction is a valid prompt. The start command's directory field is named for what it is, the output folder, before any consumer depends on the old name. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…e carries The engine reports whole milliseconds as a double, so 0.001 is the expected value, not the float 0.001f widened. Cover sub-millisecond step times, which must not round to zero. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
|
Addressed the remaining review findings in 88bb34b, 1941038 and e96689e:
Not changed: continuous-step pacing ( 🤖 Generated with Claude Code |
…steps With --continuous-pacing=window, confirming a continuous step starts the SDK step and the worker then holds it for the SDK's estimate, including the last step before export, reporting bounded progress. The default keeps the existing operator-paced order. The supervisor, page and worker take the same policy; the JSON schema is unchanged. The estimate is a time window, not completion detection, and the order is to be compared on real gloves before it becomes the default. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
|
Continuous-step pacing is now implemented as an opt-in policy in c838d2f, so the two orders can be compared on real gloves without a protocol change:
This is still unverified on hardware: whether On 🤖 Generated with Claude Code |
…mple The worker supervisor, wire protocol and MCAL storage are now a standard-library-only package beside the native worker (src/plugins/manus/calibration/python), reusable by any UI or server. The browser example keeps only its page, server and view state and depends on the package. Document that calibration is its own process with its own SDK adapter and shares no SDK code with the tracking plugin. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
The calibration worker is a separate process with its own SDK adapter and no code shared with the tracking plugin, so it now lives beside it in src/plugins/manus_calibration rather than inside src/plugins/manus. It still reuses the vendor SDK's build setup: the imported ManusSDK target is global and the plugin exports its rpath for the worker. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
|
🤖 Agent-generated (Claude Code · claude-sonnet-5-5) Re: @NVdejung's review — #1406 (review) The must-fix item (a relative `--worker` path launched through a `PATH` search) is fixed on `main` as merged in f0f5fe389: `CalibrationWorker.init` makes the executable absolute (`os.path.abspath`, `worker.py`), and `test_a_relative_worker_path_is_spawned_by_path_not_looked_up_on_PATH` covers a relative path from a changed working directory. I checked the code and the test names; I did not re-run the suite for this comment. The suggestions in that review are not addressed here. |
Summary
Adds Stage 1 of Manus glove calibration: a separate calibration worker executable in the existing Manus plugin package, and a standalone browser page that drives it. The worker writes the resulting MCAL files into a plain folder on the host the gloves are attached to, and the page lets the operator download them in the browser. Nothing here touches CaptureServer, the capture protocol, or the application frontend; those are later stages.
manus_glove_calibrator(src/plugins/manus/calibration/): a C++ executable that owns the Manus SDK connection for one attempt, reads the step list from the SDK, runs each step on operator confirmation, exports each glove's calibration straight into the folder it is given, shuts the SDK down, and only then reports a single terminal result. It is not a registered tracking plugin and publishes no device data. The wire format and step engine live in an SDK-free static library so they can be tested without hardware.examples/manus_calibration/): a plain HTML page served by a small standard-library HTTP + server-sent-events server (no UI dependency). One browser owns an attempt; the others are read-only. Every confirmation names the prompt it is for, so a click on a superseded prompt is refused. The server checks Host and Origin and requires a per-tab client id on every action, because the page can drive hardware and has no login. When a calibration completes, the page shows a download link for each generated file;GET /download/<name>serves only the files the last completed calibration reported, never anything else in the folder.--out-diris a plain folder. The supervisor hands it to the worker, which writes each file atomically (<name>.part, then a link to<attempt>_<side>.mcal) and never overwrites an existing one. There is no staging area, manifest or re-verification step. A cancelled, failed or uncertain attempt removes whatever it wrote, so only a completed calibration leaves files.Behavior to know about
startmessage names its output directorystaging_dir. The name is historical: it is the real output folder, and the README says so.Known issue
With the real SDK and gloves attached (no working Manus license on the test machine), glove discovery and the step list returned normally, and cancelling at the first prompt ended cleanly. The SDK call that starts a step then did not return within the supervisor's 30 second bound, and the cause was not established (no stack trace was obtainable). So a full calibration from the first step to saved files has not been demonstrated, and neither has loading an exported file through the tracking plugin. This PR does not claim a license failure as the root cause.
Validation
SKIP=check-copyright-year pre-commit run --all-files: all hooks passed; the copyright-year hook is skipped as in CI.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests