feat(mcp): UI-owned inbox apply (#11 slice) - #60
Conversation
jackControls
left a comment
There was a problem hiding this comment.
I found three correctness blockers:
-
Attached direct mutations are still executable. The new classification at mcp-server/src/main.rs:790 is only used by cad_submit; call_tool never enforces it. An attached client can still call a mutating tool directly, change only its private SketchManager, and recreate the silent fork this design is meant to prevent. Please incorporate or depend on #55, return the structured session_read_only error for direct mutates while attached, and add a regression covering attach -> direct mutate rejected -> cad_submit accepted -> detach restores headless mutation.
-
The generation check at src-tauri/src/session_bridge.rs:443 races the live engine. The UI publishes after a 300 ms debounce, while inbox polling runs every 250 ms. A stale-base op can therefore be applied after a local UI mutation but before heartbeat.json advances; likewise, two queued ops with the same base generation can both apply before the first publish. Please validate and advance an authoritative backend revision atomically with the live engine apply, then add deterministic tests for both races.
-
cad_submit accepts every modeling mutate, but dispatch_inbox_on_engine only maps a subset. The fallback at src-tauri/src/session_bridge.rs:385 passes the MCP tool name to host::handle; for example sketch_add_line_locked requires engine method add_line_locked, so the queued op fails and remains first forever, blocking later operations. Please share/normalize the ToolSpec name-to-engine-method and payload mapping, reject unsupported names before enqueue, and ensure a failed op is surfaced/dead-lettered rather than wedging the queue. A table-driven test should cover every accepted mutate.
Requesting changes; I did not merge.
Address Jack's three jackControls#60 blockers: 1. call_tool enforces session_read_only for direct mutates while attached (cad_submit / inspect / export stay allowed); writeback:true attach is rejected. Regression covers attach → direct mutate rejected → cad_submit accepted → detach restores headless mutation. 2. WindowPublisher.engine_revision is the authoritative OCC gate, advanced atomically with live engine apply and on immediate UI mutation notes (not heartbeat-debounce alone). Deterministic tests cover stale-base after UI note and two same-base ops. 3. Shared nbcad-mcp-mutate name→engine-method+payload map drives both cad_submit accept-list and Tauri inbox dispatch; unsupported names are rejected before enqueue; failed applies dead-letter to inbox/failed/ so the queue cannot wedge. Table-driven coverage of accepted mutates.
9fc2393 to
b6ef8be
Compare
|
Addressed the three CHANGES_REQUESTED blockers on
Tests: Please re-review; not merging. |
Stack STEP import + solid move/copy ToolSpecs (jackControls#61) onto the fixed assembly submit-map tip (jackControls#63 @ 19800b9) so cad_submit accepts solid_import_step, solid_edit_import_step, solid_move_copy, and solid_edit_move_copy while attached. cad_script and cad_compare_solids stay read-safe control helpers (not inbox mutates). Upgrade cad_script to jackControls#61 @ 52266b2 behavior: attach/refresh seeds a cad_load_project_model baseline, and the e2e test actually replays the dumped script on a fresh CadServer (detach after attach so jackControls#60 session_read_only still holds). Bump MODELING_TOOL_COUNT and keep ToolSpec↔map sync.
jackControls
left a comment
There was a problem hiding this comment.
Re-reviewed at b6ef8be. The earlier direct-mutation bypass is fixed, but two blocking correctness gaps remain. First, local UI mutations advance the session engine_revision only through a later fire-and-forget JavaScript noteEngineRevision call. Between native mutation completion and that second command, inbox apply can acquire the publisher lock and accept a stale-base operation. Please advance or check the authoritative revision in the same native critical section or transaction as the engine mutation. Second, the lowest conflicting or malformed inbox entry remains pending and is retried forever. A refreshed operation submitted at the next sequence cannot progress because the stale entry remains at the head. Please dead-letter or archive it, explicitly replace or rebase it, or otherwise resolve invalid and conflicting head entries. Current tests note mutation before queueing and explicitly expect a conflicting sequence to remain pending, so they do not cover these production failure windows.
Address Jack's three jackControls#60 blockers: 1. call_tool enforces session_read_only for direct mutates while attached (cad_submit / inspect / export stay allowed); writeback:true attach is rejected. Regression covers attach → direct mutate rejected → cad_submit accepted → detach restores headless mutation. 2. WindowPublisher.engine_revision is the authoritative OCC gate, advanced atomically with live engine apply and on immediate UI mutation notes (not heartbeat-debounce alone). Deterministic tests cover stale-base after UI note and two same-base ops. 3. Shared nbcad-mcp-mutate name→engine-method+payload map drives both cad_submit accept-list and Tauri inbox dispatch; unsupported names are rejected before enqueue; failed applies dead-letter to inbox/failed/ so the queue cannot wedge. Table-driven coverage of accepted mutates.
b6ef8be to
30785fe
Compare
|
Addressed both remaining blockers on 1. Authoritative revision in the same native critical sectionUI mutates no longer rely on a later fire-and-forget JS
2. Stuck / conflicting / malformed head
Pending queue advances so a later refreshed seq can apply. Tests no longer expect a conflicting head to remain pending forever. Tests
Please re-review; not merging. #65 untouched. |
Stack assembly ToolSpecs (jackControls#62) onto the UI-owned apply / mcp-mutate branch (jackControls#60) so cad_submit accepts assembly_create_component, assembly_update_component, assembly_create_occurrence, assembly_update_occurrence, assembly_set_occurrence_pose, and assembly_set_occurrence_grounded while attached. assembly_document and assembly_solution stay read-safe inspect tools. Add Assembly focus pack and classifier/roundtrip coverage.
There was a problem hiding this comment.
Re-reviewed at 30785fe. The native mutation critical section and dead-letter queue fix the earlier blockers, and the focused bridge tests pass. Two P1 coherency races remain:
-
publishNowreserves a generation, then exports the live engine state, then writes the snapshot in three separate awaits. A UI mutation can complete after export but before write, advanceengine_revision, and the stale export is still accepted and published at that current revision. An MCP client can then author against stale geometry and pass the base-generation check. The reservation needs to capture the engine revision and the write must reject/retry if it changed during export, or export/write must share the native publisher-to-engine critical section. Please add a deterministic mutation-between-export-and-write test. -
Project-session bind/create/activate/drop bypass
SessionBridgeState, while inbox dispatch always targetsAppState.active_mut(). If an op was queued for tab A and the user activates retained tab B before the next debounced publish, the old base can still match and the op applies to B. Please bind the bridge to the active native project-session identity and reject mismatches, and advance/rebind it atomically on project-session transitions. Add an A-to-B switch test that proves an A op cannot mutate B.
Requesting changes until both target-coherency gaps are closed.
Address Jack's three jackControls#60 blockers: 1. call_tool enforces session_read_only for direct mutates while attached (cad_submit / inspect / export stay allowed); writeback:true attach is rejected. Regression covers attach → direct mutate rejected → cad_submit accepted → detach restores headless mutation. 2. WindowPublisher.engine_revision is the authoritative OCC gate, advanced atomically with live engine apply and on immediate UI mutation notes (not heartbeat-debounce alone). Deterministic tests cover stale-base after UI note and two same-base ops. 3. Shared nbcad-mcp-mutate name→engine-method+payload map drives both cad_submit accept-list and Tauri inbox dispatch; unsupported names are rejected before enqueue; failed applies dead-letter to inbox/failed/ so the queue cannot wedge. Table-driven coverage of accepted mutates.
30785fe to
f241739
Compare
|
Rebased onto
|
Rebase jackControls#63 onto jackControls#60 tip f241739. Mutate map entries and ToolSpecs already landed with jackControls#60/jackControls#62; keep unique classifier/lookup coverage so cad_submit accepts the six assembly modeling mutates while assembly_document and assembly_solution stay read-safe inspect tools.
Address Jack's three jackControls#60 blockers: 1. call_tool enforces session_read_only for direct mutates while attached (cad_submit / inspect / export stay allowed); writeback:true attach is rejected. Regression covers attach → direct mutate rejected → cad_submit accepted → detach restores headless mutation. 2. WindowPublisher.engine_revision is the authoritative OCC gate, advanced atomically with live engine apply and on immediate UI mutation notes (not heartbeat-debounce alone). Deterministic tests cover stale-base after UI note and two same-base ops. 3. Shared nbcad-mcp-mutate name→engine-method+payload map drives both cad_submit accept-list and Tauri inbox dispatch; unsupported names are rejected before enqueue; failed applies dead-letter to inbox/failed/ so the queue cannot wedge. Table-driven coverage of accepted mutates.
f241739 to
5526bf0
Compare
|
Rebased onto latest main ( New tip: |
Rebase jackControls#63 onto jackControls#60 tip f241739. Mutate map entries and ToolSpecs already landed with jackControls#60/jackControls#62; keep unique classifier/lookup coverage so cad_submit accepts the six assembly modeling mutates while assembly_document and assembly_solution stay read-safe inspect tools.
cad_submit writes inbox/<seq>.json; the desktop applies via host::handle and publishes. MCP does not write model.json (no last-writer-wins).
Address Jack's three jackControls#60 blockers: 1. call_tool enforces session_read_only for direct mutates while attached (cad_submit / inspect / export stay allowed); writeback:true attach is rejected. Regression covers attach → direct mutate rejected → cad_submit accepted → detach restores headless mutation. 2. WindowPublisher.engine_revision is the authoritative OCC gate, advanced atomically with live engine apply and on immediate UI mutation notes (not heartbeat-debounce alone). Deterministic tests cover stale-base after UI note and two same-base ops. 3. Shared nbcad-mcp-mutate name→engine-method+payload map drives both cad_submit accept-list and Tauri inbox dispatch; unsupported names are rejected before enqueue; failed applies dead-letter to inbox/failed/ so the queue cannot wedge. Table-driven coverage of accepted mutates.
Advance session engine_revision in run_ui_mutation under the publisher lock across the live engine call so inbox OCC cannot race a later JS note. Dead-letter conflicting and malformed inbox heads to inbox/failed/ so the queue cannot wedge; next sequence can apply. Sync jackControls#61 mutates (move_copy/import_step) into the shared map after rebase onto main.
Reserve captures engine_revision and write rejects a snapshot if it moved during export. Bind the bridge to the active native project-session identity so an A inbox op cannot apply to B.
5526bf0 to
25bdf45
Compare
Rebase jackControls#63 onto jackControls#60 tip f241739. Mutate map entries and ToolSpecs already landed with jackControls#60/jackControls#62; keep unique classifier/lookup coverage so cad_submit accepts the six assembly modeling mutates while assembly_document and assembly_solution stay read-safe inspect tools.
|
Rebased onto latest main ( |
jackControls
left a comment
There was a problem hiding this comment.
Thanks for the repeated rebases and for adding native project/session binding. I rechecked the current head (25bdf45) and reran the focused bridge and MCP suites. Two concurrency blockers still remain:
-
Publication reservation and write are not bound to the same project/session identity. The reservation response includes
session_idandproject_session_id, but the frontend write payload carries only the generation and exported model/focus. The backend then resolvespublisher.active_mut()at write time. If tab A reserves generation N, tab B becomes active and also reserves generation N, and A's delayed export then writes, that payload can consume B's reservation and publish A's model into B's session. The inbox-side project binding does not protect this publication path. Please carry the reserved project/session identity through the write and reject a write if it no longer matches. A deterministic reserve-A → activate-B → reserve-B → write-A regression test would cover this. -
Concurrent
cad_submitcalls can allocate the same inbox sequence and overwrite one operation.next_inbox_seqscans for max + 1, then the operation is written separately. Two MCP processes can choose the same sequence and both report success while one rename replaces the other. Please make sequence reservation/write exclusive (for example, an atomic create or per-session lock) and add a contention test proving every accepted operation gets a distinct durable entry.
The existing focused tests pass, but they do not exercise either interleaving. These need to be resolved before this bridge is safe for simultaneous tabs/agents.
Carry reserved session_id + project_session_id on the write path so a delayed A export cannot consume B's reservation. cad_submit allocates inbox sequences with create_new so concurrent submits cannot share a seq.
|
Addressed the two remaining concurrency P1s at 1. Publish write is bound to the reserved identity
Test: 2. Exclusive inbox sequence allocation
Test: Tests
Please re-review; not merging. |
Rebase jackControls#63 onto jackControls#60 tip f241739. Mutate map entries and ToolSpecs already landed with jackControls#60/jackControls#62; keep unique classifier/lookup coverage so cad_submit accepts the six assembly modeling mutates while assembly_document and assembly_solution stay read-safe inspect tools.
jackControls#68 adds host-neutral assembly_create_joint / assembly_update_joint. Stack those ToolSpecs on jackControls#60's MUTATES map so cad_submit accepts them while attached. assembly_document / assembly_solution stay read-safe.
jackControls
left a comment
There was a problem hiding this comment.
Thanks for the update. I rechecked both prior P1 races: the write is now bound to the reserved project/session identity, and inbox allocation uses exclusive create-with-retry. The new cross-tab and concurrent-allocation tests exercise the exact failure modes, and the focused local tests plus all CI jobs pass. Approved.
Stack those ToolSpecs on jackControls#60's MUTATES map so cad_submit accepts them while attached. assembly_document / assembly_solution stay read-safe.
Summary
model.jsonwriteback (Jack removed that; not brought back).cad_submit(attached only) writesinbox/<seq>.jsonas{ name, arguments, base_generation }. Does not mutate the MCP in-memory document.base_generation→ structured{code:"generation_conflict", writeback:false, session_mode:"ui_owned_apply", ...}. Submit of inspect/export/control is rejected (same deny-list idea as fix(mcp): reject mutations while snapshot-attached (tutor-safe #11 slice) #55is_read_safe_while_attached). Headless (no attach) still mutates locally.apply_inbox_opinmcp-server/src/session.rsreads one inbox file, checks generation, callshost_apply, archives the op. Caller publishes the model.mcp_session_bridge_apply_inboxdispatches on the liveAppState(host::handle/ solid-replay, same path as IPC).src/sessionBridge.tspolls it every 250ms fromstartSessionBridge()(already invoked fromsrc/main.tsx). After apply,applySolidUpdate/loadDocumentruns and the existing publisher writes the new snapshot.Why
#55 (still open) rejects in-memory MCP mutations while attached. Acceptance still missing: MCP solid op appears in the UI document. Jack-happy rule: desktop/engine is the only writer of the live document. MCP may only submit.
Test plan
cargo test --manifest-path mcp-server/Cargo.toml --bin nbcad-mcp snapshot -- --test-threads=1attach_cad_submit_writes_inbox_without_mutating_memory,apply_inbox_helper_on_separate_manager_then_refresh_sees_body,stale_base_generation_is_generation_conflict_and_does_not_apply,cad_submit_without_attach_fails,headless_goldens_still_mutate_without_attach,inbox_write_and_stale_apply_are_generation_lockednbcad-mcpunit suite: 34 passedcad_submit(solid_extrude|solid_mirror), confirm the body appears in the viewport, thencad_refreshsees the same snapshotsession_bridgetests (this box has no GTK/pkg-config, sosrc-tauridid not compile here)What is still missing for “MCP extrude visible in UI”
cad_submit(notsolid_extrudeon the attached MCP copy). fix(mcp): reject mutations while snapshot-attached (tutor-safe #11 slice) #55 is still the in-memory lock; this PR does not merge it.cad_refresh(no watch).host::handleby name and may need a mapping if they miss.What this is not
Refs
#11 #55