{"slug": "treat-per-task-model-switching-as-a-concurrency-protocol", "title": "Treat Per-Task Model Switching as a Concurrency Protocol", "summary": "A developer at Chaitin MonkeyCode proposes treating per-task model switching as a concurrency protocol to prevent race conditions. The approach uses generation-based guards to ensure only the latest request's model is applied, with a simulator demonstrating the issue and a solution. The design includes contracts for duplicate requests, competing requests, late successes, and crash recovery.", "body_md": "Changing the model for a running AI task is not a settings update. It is a distributed operation:\n\n``` php\nread current task -> prepare credentials/config -> request restart -> receive result -> persist active model\n```\n\nIf two switches overlap, completion order can differ from request order. The system needs a rule for which intent wins.\n\nAt commit [ c58bcd4](https://github.com/chaitin/MonkeyCode/tree/c58bcd4dd4b7031f469a1271f276d22550b8f523),\n\n`TaskModelSwitch`\n\nThe reviewed [task use case](https://github.com/chaitin/MonkeyCode/blob/c58bcd4dd4b7031f469a1271f276d22550b8f523/backend/biz/task/usecase/task.go) creates a switch record, asks taskflow to restart with the target model configuration, and completes the switch record and task model based on the response. The accompanying tests cover success and failure paths.\n\nFrom this source review, I could not establish an explicit compare-and-swap generation or a per-task serialization contract around overlapping requests. That does **not** prove an exploitable race: serialization may exist elsewhere in the deployment or taskflow boundary. It means concurrency semantics deserve an explicit test and contract.\n\nAssume request A selects model A, then request B selects model B:\n\n``` php\ntime ->\nA: request ---- restart ---------------- complete\nB:        request -- restart -- complete\n```\n\nIf each successful completion writes its model, B applies first and late A overwrites it. Reverse network timing and the result changes.\n\nThe companion simulator makes that order dependence visible:\n\n``` js\nexport function naiveCompletionOrder(completions) {\n  let model = \"initial\";\n  for (const completion of completions) {\n    if (completion.success) model = completion.model;\n  }\n  return model;\n}\n```\n\n`[A, B]`\n\nends on B. `[B, A]`\n\nends on A. The caller's latest intent is not part of the rule.\n\nAssign a generation while accepting each request:\n\n``` php\nA -> generation 41\nB -> generation 42\n```\n\nCompletion may update active state only when its generation equals the task's current requested generation:\n\n```\nUPDATE tasks\nSET active_model_id = :model,\n    applied_generation = :generation\nWHERE id = :task_id\n  AND requested_generation = :generation;\n```\n\nZero updated rows means the operation was superseded. Keep its history, but do not apply stale state.\n\nThe minimal `GenerationSwitch`\n\nclass in the companion artifact implements that rule. Run:\n\n```\nnode test-model-switch.mjs\n```\n\nExpected output:\n\n```\nPASS naive result depends on completion order; generation guard preserves newest request\n```\n\nThe test deliberately completes request B before A and proves that late A is marked `superseded`\n\nwhile model B remains active.\n\nDefine all of these:\n\n| Concern | Contract |\n|---|---|\n| Duplicate request | Same request ID returns the same operation, without a second restart |\n| Competing request | New generation supersedes old intent, or admission rejects while busy |\n| Late success | Recorded for audit; cannot overwrite a newer generation |\n| Restart failure | Active model remains the last successfully applied generation |\n| Process crash | Reconciler compares requested, applied, and runtime-observed state |\n| Session loading | Bound to the specific operation and generation |\n| Credential binding | Runtime token and cached config correspond to the applied model |\n\nSerialization is a valid alternative: acquire a per-task lock or queue switches. It simplifies overlap but requires lease expiry, crash recovery, fairness, and a user-visible “switch in progress” state. A generation guard remains useful as defense against stale workers.\n\nUnit tests should pause operations at credential creation, switch-record creation, restart dispatch, restart response, and persistence. Release A and B in every relevant order. Assert:\n\nThe invariant is concise:\n\n```\nactive model == successful result for the greatest non-superseded generation\n```\n\nOnce model switching is treated as a protocol, UI labels, audit records, retries, tokens, and persistence can all agree on what “current” means.\n\nDisclosure: I contribute to the MonkeyCode project. Current fields and flow are based on the linked source at commit\n\n`c58bcd4`\n\n. The concurrency risk is a bounded source-review question, not a confirmed vulnerability; the generation/CAS simulator was tested locally.", "url": "https://wpnews.pro/news/treat-per-task-model-switching-as-a-concurrency-protocol", "canonical_source": "https://dev.to/robinzzz/treat-per-task-model-switching-as-a-concurrency-protocol-33mp", "published_at": "2026-07-14 06:19:47+00:00", "updated_at": "2026-07-14 06:29:09.095387+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Chaitin MonkeyCode", "TaskModelSwitch"], "alternates": {"html": "https://wpnews.pro/news/treat-per-task-model-switching-as-a-concurrency-protocol", "markdown": "https://wpnews.pro/news/treat-per-task-model-switching-as-a-concurrency-protocol.md", "text": "https://wpnews.pro/news/treat-per-task-model-switching-as-a-concurrency-protocol.txt", "jsonld": "https://wpnews.pro/news/treat-per-task-model-switching-as-a-concurrency-protocol.jsonld"}}