Why
agent_instances.config is one JSON blob, and 16 call sites read it whole, mutate one key,
and write it whole:
routes/instances-apply.ts · routes/instances-translation.ts · routes/tools.ts
routes/instances-behaviour.ts · routes/coding.ts · routes/instances.ts
lib/behaviour-store.ts · lib/board.ts
Two concurrent requests touching different keys therefore lose one of them: both read the
same pre-state, and the second write clobbers the first.
This is not hypothetical — it already shipped a user-visible bug. The Behaviour tab's "Reset
group" fired one PUT per field; all five read the same config and the last write won, so it
cleared exactly one field, apparently at random. Fixed for that one caller (5365774) by batching
into a single request, but the underlying pattern is untouched and every other caller has it.
Realistic collisions today, none needing an unusual race:
- The agent calls
set_behaviour (or configure_board, or a settings write) while the owner
saves something in the console. - A trigger-driven pipeline writes config while the owner edits Settings.
- Two browser tabs open on the same instance.
The loss is silent — no error, no conflict, the setting just doesn't stick.
Options
- Optimistic concurrency — add a
config_version column, UPDATE … WHERE config_version = ?, retry on 0 rows. Correct, one migration, every write site changes. - Single-key writes in SQL —
json_set(config, '$.behaviour', ?) so a write only touches
its own subtree. No migration; each site becomes a targeted patch. Doesn't help two writers of
the same key, which is the rarer and less harmful case. - One helper all sites must go through (
patchInstanceConfig(env, id, uid, key, value)),
implementing whichever of the above is chosen, plus a lint/test forbidding raw
UPDATE agent_instances SET config elsewhere.
(3) + (2) is probably the cheapest correct answer.
Verification
- A test that fires two concurrent patches to different keys and asserts BOTH survive. It must
fail against today's code — otherwise it isn't reproducing the bug.
Why
agent_instances.configis one JSON blob, and 16 call sites read it whole, mutate one key,and write it whole:
Two concurrent requests touching different keys therefore lose one of them: both read the
same pre-state, and the second write clobbers the first.
This is not hypothetical — it already shipped a user-visible bug. The Behaviour tab's "Reset
group" fired one PUT per field; all five read the same config and the last write won, so it
cleared exactly one field, apparently at random. Fixed for that one caller (5365774) by batching
into a single request, but the underlying pattern is untouched and every other caller has it.
Realistic collisions today, none needing an unusual race:
set_behaviour(orconfigure_board, or a settings write) while the ownersaves something in the console.
The loss is silent — no error, no conflict, the setting just doesn't stick.
Options
config_versioncolumn,UPDATE … WHERE config_version = ?, retry on 0 rows. Correct, one migration, every write site changes.json_set(config, '$.behaviour', ?)so a write only touchesits own subtree. No migration; each site becomes a targeted patch. Doesn't help two writers of
the same key, which is the rarer and less harmful case.
patchInstanceConfig(env, id, uid, key, value)),implementing whichever of the above is chosen, plus a lint/test forbidding raw
UPDATE agent_instances SET configelsewhere.(3) + (2) is probably the cheapest correct answer.
Verification
fail against today's code — otherwise it isn't reproducing the bug.