{"slug": "remote-200-local-exit-1-store-the-turn-id-on-both-sides", "title": "Remote 200, Local Exit 1. Store the Turn Id on Both Sides.", "summary": "A developer proposes storing a shared turn_id on both remote inference logs and local tool spans to close a gap where an agent's remote log recorded HTTP 200 while the local tool span recorded exit code 1, yet the agent summary still reported the edit landed cleanly. The note offers a join-record schema, a validation checker, four mismatch classes and a debug loop, with code presented as an unexecuted example rather than a benchmark.", "body_md": "A coding agent writes a three-file patch during a long run. The remote inference log records HTTP 200 for that turn. The local tool span records exit code 1 for the patch.\n\nThe agent summary still reports that the edit landed cleanly. No shared identifier connects those three records together. The failure sits in the gap between the two logs.\n\nRecent community posts spend their energy on model choice. This note does not rank models or repeat those arguments. It asks whether both logs can name the same attempt.\n\nLocal traces usually name tools, arguments, and exit codes. Remote logs usually name model calls, status, and latency. Each side keeps its own clock and its own identifier space.\n\nA retry writes a second model call under a new request id. The tool span can still point at the first attempt only. A file diff will not show that the join itself broke.\n\nThis split appears on long agent runs with several tools. It appears again when the runner and the server sit far apart. Delay makes the timelines look unrelated even when they match.\n\nThis note gives you a join record and a small checker. It also gives you four mismatch classes and a debug loop. The code is a proposed example and was not executed here.\n\nIt is not a benchmark of any model or any server. It does not quote a token allowance or a hardware spec. Treat fixture counts as labels for the method, not as measurements.\n\nStore one join row for every model turn and every tool span.\n\n`trace_id` identifies the whole agent run from start to finish.`turn_id` identifies one model request and the matching response.`tool_call_id` identifies one tool call, or stays null on model rows.`parent_turn_id` names the model turn that requested that tool.`attempt` is an integer that starts at 1 for each logical turn.`side` is either `model` or `tool`, and no third value is valid.` server_request_id` stores the remote id, or null when that id is absent.`status` is a short code such as `ok` or `error`.` local_mono_ns` stores local monotonic time, counted in nanoseconds.\nDo not store raw prompts or tool output in this join file. Store a content hash when you need an equality check later. Keep secrets out of the file before you export it anywhere.\n\nA checker that guesses parents will hide emitter bugs. Fail closed when a required field is missing or contradictory. Do not repair a row by matching the nearest timestamp.\n\n``` python\n# Proposed example. Not executed in this draft.\nfrom dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass JoinRow:\n    trace_id: str\n    turn_id: str\n    tool_call_id: str | None\n    parent_turn_id: str | None\n    attempt: int\n    side: str\n    server_request_id: str | None\n    status: str\n    local_mono_ns: int\n\ndef validate(row: JoinRow) -> list[str]:\n    errors: list[str] = []\n    if not row.trace_id or not row.turn_id:\n        errors.append(\"missing_ids\")\n    if row.attempt < 1:\n        errors.append(\"bad_attempt\")\n    if row.side not in {\"model\", \"tool\"}:\n        errors.append(\"bad_side\")\n    if row.side == \"tool\" and not row.parent_turn_id:\n        errors.append(\"tool_without_parent_turn\")\n    if row.side == \"tool\" and not row.tool_call_id:\n        errors.append(\"tool_without_tool_id\")\n    if row.side == \"model\" and row.tool_call_id is not None:\n        errors.append(\"model_row_has_tool_id\")\n    if row.local_mono_ns < 0:\n        errors.append(\"bad_clock\")\n    return errors\n```\n\nRun `validate` on every row before the join step starts. Stop the report when any row returns a schema error. A partial join over dirty input looks precise and is not.\n\nIndex model rows by trace id, turn id, and attempt. Look up each tool span by its parent turn id. Return one class string, and do not attach a score.\n\n``` python\n# Proposed example. Not executed in this draft.\ndef index_models(rows: list[JoinRow]) -> dict[tuple[str, str, int], JoinRow]:\n    indexed = {}\n    for row in rows:\n        if row.side != \"model\":\n            continue\n        indexed[(row.trace_id, row.turn_id, row.attempt)] = row\n    return indexed\n\ndef classify(tool: JoinRow, models: dict) -> str:\n    if tool.side != \"tool\":\n        return \"not_a_tool_row\"\n    if not tool.parent_turn_id:\n        return \"orphan_tool\"\n    key = (tool.trace_id, tool.parent_turn_id, tool.attempt)\n    parent = models.get(key)\n    if parent is None:\n        return \"missing_model_row\"\n    if parent.server_request_id and tool.server_request_id:\n        if parent.server_request_id != tool.server_request_id:\n            return \"request_id_mismatch\"\n    if parent.status != \"ok\" and tool.status == \"ok\":\n        return \"tool_ok_after_model_error\"\n    return \"joined\"\n```\n\nEmpty parent ids should die in validation, not in classify. The orphan class remains for checkers that skipped that gate. The checker does not rank models and does not score patches.\n\nIt only reports whether the two logs agree on identity. A joined pair can still contain a wrong edit. Agreement is a precondition, not a verdict on the patch.\n\n| Class | Local evidence | Remote evidence | First check | \n|---|---|---|---|\n| `orphan_tool` | tool span, empty parent | nothing required | validation should already have failed | \n| `missing_model_row` | parent id is set | no matching model row | dropped log, wrong id, or sampling | \n| `request_id_mismatch` | server id A on the tool | server id B on the model | a retry wrote a new call | \n| `tool_ok_after_model_error` | tool status is `ok` | model status is not `ok` | summary trusted the wrong side | \n\nRead the table from the class column toward the check column. Fix the emitter before you rewrite the prompt or the tool. A missing remote row is not proof that the call never ran.\n\nUse a tiny fixture so the four classes stay reviewable. Label it as synthetic data inside the test file itself. Do not paste these counts into a status report as results.\n\n`request_id_mismatch`, `missing_model_row`, and `joined`.\n\n``` php\n# Proposed fixture. Not executed in this draft.\ndef test_fixture_classes() -> None:\n    rows = [\n        JoinRow(\"run_1844\", \"turn_a\", None, None, 1, \"model\", \"req_1\", \"ok\", 10),\n        JoinRow(\"run_1844\", \"turn_a\", None, None, 2, \"model\", \"req_2\", \"ok\", 20),\n        JoinRow(\"run_1844\", \"tool_row_1\", \"tool_1\", \"turn_missing\", 1, \"tool\", \"req_x\", \"error\", 30),\n        JoinRow(\"run_1844\", \"tool_row_2\", \"tool_2\", \"turn_a\", 1, \"tool\", \"req_9\", \"ok\", 12),\n        JoinRow(\"run_1844\", \"tool_row_3\", \"tool_3\", \"turn_a\", 2, \"tool\", \"req_2\", \"ok\", 21),\n    ]\n    assert all(validate(row) == [] for row in rows)\n    models = index_models(rows)\n    got = [classify(row, models) for row in rows if row.side == \"tool\"]\n    assert sorted(got) == [\"joined\", \"missing_model_row\", \"request_id_mismatch\"]\n```\n\nThe fixture expects those mismatch classes to appear. The gate command below is for a later run, after the emitter fix. Do not point the gate at this fixture and call the failure a product bug.\n\n```\n# Proposed commands. Paths are examples only.\npython join_check.py --local runs/trace.jsonl --remote runs/inference.jsonl --out runs/join-report.json\npython join_check.py --local runs/trace.jsonl --remote runs/inference.jsonl --expect-zero orphan_tool,request_id_mismatch\n```\n\nThe second command is a regression gate for the emitter. It should fail while those two mismatch classes still appear. Keep missing model rows out of that gate until retention is known.\n\nWrite the checker output as a small JSON object. Mark synthetic runs so nobody quotes them as production data. Include the input file hash so a later replay can detect edits.\n\n```\n{\n  \"trace_id\": \"run_1844\",\n  \"fixture\": true,\n  \"schema_errors\": 0,\n  \"input_sha256\": \"replace-with-real-hash\",\n  \"counts\": {\n    \"joined\": 1,\n    \"orphan_tool\": 0,\n    \"missing_model_row\": 1,\n    \"request_id_mismatch\": 1,\n    \"tool_ok_after_model_error\": 0\n  }\n}\n```\n\nKeep both input files immutable during that replay. Write each report to a new path with the trace hash in the name. That habit stops a later edit from rewriting the evidence.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nThe operator describes MonkeyCode as an open-source agent project. The same brief says free model access and a free server option exist. This draft does not state a token cap or a model list.\n\nIt also omits machine size and any uptime promise. Those access details change, and stale numbers mislead planning. Confirm the current limits in the project documentation before a long run.\n\nDo not treat a blog post, including this one, as the pricing source. A free server helps this loop in one narrow way. You can place the agent loop on that server instead of the laptop.\n\nYou still keep the local join file under your control. That file remains the record you hold when remote retention is short. Free access does not remove the two-log problem.\n\nA remote log can rotate before you export it. A rate limit can create attempt 2 with no matching tool span. Record attempt on both sides or you will join the wrong call.\n\nEcho the server request id into the local row when one is returned. If the server cannot echo the turn id, stop and fix that contract. Use the free option as a second environment, not as an oracle.\n\nRun the same schema in both places and diff the class counts. Diff those class-count reports, not the product pages. A lower count means the emitter improved, not that the model improved.\n\nDo not upload raw prompts just to fill an empty join field. Redact secrets before any export to a shared or free server. If redaction is unclear, keep the join file on the runner.\n\nSkip it when one process already writes one complete log. Skip it when you cannot change the emitter that mints ids. Skip it when the question is model quality rather than trace integrity.\n\nA fixed task harness answers quality questions better than this checker. This checker answers whether the two logs describe the same attempt. Mixing those questions produces a confident report about the wrong fault.\n\nAlso skip the remote half when the server log is heavily sampled. Say that in the report so a reader does not chase empty rows. Local validation can still run alone in that sampled case.\n\nPick one failed run that already has a local JSONL trace. Add the turn id and the parent turn id at the emitter. Classify once, and keep that report beside the trace as a baseline.\n\nIf you use MonkeyCode's described free server option, copy the request id. Put that id into the same local join row before you export. Check current access limits in the project docs before the second run.\n\nRepeat the six steps in that second environment. Stop when the gated classes hit zero, then reread the patch. The join is ready only when the same attempt is named on both sides.", "url": "https://wpnews.pro/news/remote-200-local-exit-1-store-the-turn-id-on-both-sides", "canonical_source": "https://dev.to/apprs_6334/remote-200-local-exit-1-store-the-turn-id-on-both-sides-4f3j", "published_at": "2026-10-10 17:39:59+00:00", "updated_at": "2026-10-10 17:46:25.011777+00:00", "lang": "en", "topics": ["ai-agents", "mlops", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/remote-200-local-exit-1-store-the-turn-id-on-both-sides", "markdown": "https://wpnews.pro/news/remote-200-local-exit-1-store-the-turn-id-on-both-sides.md", "text": "https://wpnews.pro/news/remote-200-local-exit-1-store-the-turn-id-on-both-sides.txt", "jsonld": "https://wpnews.pro/news/remote-200-local-exit-1-store-the-turn-id-on-both-sides.jsonld"}}