You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG][regression] Global _index.json full rewrite is unsafe under concurrent tasks (real corruption under JetBrains multi-agent) #1231
Task history persistence uses a global _index.json updated via
read → mutate entire entries array → atomic full-file rewrite (safeWriteJson).
That is safe for a single writer, not for concurrent agents.
We are already seeing production corruption with concurrent agents on the
JetBrains plugin (not observed on last Roo Code CE with serial usage).
This blocks trustworthy parallelizable tasks :
runtime pieces (TaskRegistry, TaskScheduler, TaskSemaphore, status machine)
increase concurrent writers; storage still assumes one writer.
Failure mode
Task A reads index (version V).
Task B reads index (version V).
A writes V+A (atomic rename — OK in isolation).
B writes V+B and drops A’s updates.
safeWriteJson prevents half-written JSON; it does not prevent lost updates.
(Real-life situation is probably more complex, as ui_message.json and history_item.json also get corrupted if one opens a task in the UI!)
More parent/child fields written on delegate/return/cancel
TaskRegistry / scheduler / semaphore → more concurrent activity
Storage model remained a single global mutable JSON document.
Proposal (phased)
P0 – stop the bleeding
Optimistic concurrency (occ): record file identity on read (size/mtime/inode);
refuse write if changed; retry read-patch-write for single-entry updates.
Process-wide mutex around index updates (same extension host).
Prefer updating tasks/<id>/history_item.json as source of truth; treat _index.json as a rebuildable cache where possible.
Append-only event log (JSONL or line protocol) or SQLite (WAL) task catalog.
Repository interface shared by GUI, CLI, and repair tools.
Evidence / tooling
Repair CLI that rebuilds index from per-task files and detects corruption.
Snapshot-checked writer prototype (mtime/ctime/size/inode) that turns
silent clobber into Concurrent modification detected.
Ask
Treat concurrent history writes as a release blocker for parallel tasks,
not a rare edge case. JetBrains multi-agent already proves the race in the wild.
changed the title [-][BUG][regression] Concurrent writes corrupt global _index.json[/-][+][BUG][regression] Global _index.json full rewrite is unsafe under concurrent tasks (real corruption under JetBrains multi-agent)[/+]on Aug 12, 2026
I landed the in-process withLock work in #912; this is the same lost-update problem at process scope, and the harness above confirms it — the _index.json read-modify-write is guarded only by safeWriteJson's per-file lockfile. I'd take the P0 shape building on #1210: a cross-process lock covering read/merge/write (or OCC with retry), the stale-snapshot case as a regression test in the deterministic-promise style, and index reconciliation from history_item.json as a follow-up if you want it.
Draft #926 is narrower and idle since July — happy to fold it in or supersede it, whichever you prefer.
Thank you guys for looking into this. I am not sure if the race on writing on _index.json is the only issue and everything else I observed is just a consequence of this.
Or maybe the issue is broader, or - as @morgan-coded pointed out - the same issue on the history_item.json (HI).
I just want to describe a bit more on the symtoms:
I found out about the corruption by using zoo-code plugin in Jetbrains IDEs. I had migrated from the Roo Code CE plugin in two instances (Webstorm and Phpstorm). I hence had it running in both IDEs at the same time. Although working on different workspaces altogether. So - apart from _index.json (IDX) - there was no overlap in tasks being updated.
Once I opened the plugin in the second IDE i could see the task history as usual. I am not sure if I first opened a root task from the offered list, or if I already opened the subtask list and jumped straight to the subtask of the root process. However, once I opened the subtask, nothing was shown in the task history. Just a blank page. So I wanted to go back to the parent. Turned out that the subtask - after opening - was shown with "Task #0 (Incomplete)" label once I got back to the history index. Opening a second subtask of the same parent produced the same issue. I think I end it up having
1 task with Task #x (No messages)
2 tasks with Task #0 (Incomplete)
So I stopped opening any other task as the history data was crucial to me. Then I found that in at least one subtask directory, the ui_messages.json (UIM) was gone. In at least one other, it was set to [].
I've been able to rebuild everything from the api_conversation_history.json (ACH) using the zoo-code-history-repair tool I created (apart from the MCP Server communication which is only stored in UIM). However, I can't really explain why the "Task #0 (Incomplete)". Analysing the code, the only failure-path would be a reading error on the UIM file. Which I find weird, as it was just one single instance that was trying to open and display the task.
In the wake of all this, the following values of HI got reset to 0:
tokensIn
tokensOut
totalCost
size
cacheWrites
cacheReads
which obviously also ended up in the global IDX (including the truncated task field)
I'm with you to first trying to stop this from happening with the current setup. I'm not sure if the approach I use in saveFile with the FileSnapshot would be a feasible approach.
But I think it is also important to think of ways to improve the way the logging is happening. In my opinion, there are several design issues:
for every update, a full read, parse, append, stringify, write cycle is required. This is insane. An append-only approach should be used instead.
having a global index does not provide a real benefit in real-world use. tasks are (at least currently) single-workspace confined. having one index per wrokspace (eg. using a sha1-hash of the workspace path as an identifier) would take away the pressure of this one single global index which needs to be updated on EVERY agent turn of EVERY task running. So tasks compete to write to one file, however the domains they want to essentially write to do NOT overlap at all. Separating them into individual workspaces would solve this. this, by the way, still applies even if we solve the initial race issue. because even if we can make writing safely, there will still be races happening and hence index-writing delayed. baking off from index writing needs to use randomized wating times, so the chance increases that they don't collide on the next attempt to place the upsert.
as a durable solution I would suggest to evaluate the usage of sqlite databases. there is duplicated data in ACH and UIM and HI, that needs to be kept in sync. storing every peace of info in only one place would greatly simplify maintaining the data. sqlite already manages locking natively. It allows to reads to processes while another is writing, it supports transactions. So having one db per task or per workspace. one per task seems good in terms of size, easier to delete a task. for Global Search a single database would be best, or a single database per workspace. so this needs careful evaluation.
Activity
[-][BUG][regression] Concurrent writes corrupt global _index.json[/-][+][BUG][regression] Global _index.json full rewrite is unsafe under concurrent tasks (real corruption under JetBrains multi-agent)[/+]morgan-coded commentedon Aug 16, 2026
Claiming this.
I landed the in-process
withLockwork in #912; this is the same lost-update problem at process scope, and the harness above confirms it — the_index.jsonread-modify-write is guarded only bysafeWriteJson's per-file lockfile. I'd take the P0 shape building on #1210: a cross-process lock covering read/merge/write (or OCC with retry), the stale-snapshot case as a regression test in the deterministic-promise style, and index reconciliation fromhistory_item.jsonas a follow-up if you want it.Draft #926 is narrower and idle since July — happy to fold it in or supersede it, whichever you prefer.
martin-rueegg commentedon Aug 16, 2026
Thank you guys for looking into this. I am not sure if the race on writing on _index.json is the only issue and everything else I observed is just a consequence of this.
Or maybe the issue is broader, or - as @morgan-coded pointed out - the same issue on the
history_item.json(HI).I just want to describe a bit more on the symtoms:
I found out about the corruption by using zoo-code plugin in Jetbrains IDEs. I had migrated from the Roo Code CE plugin in two instances (Webstorm and Phpstorm). I hence had it running in both IDEs at the same time. Although working on different workspaces altogether. So - apart from
_index.json(IDX) - there was no overlap in tasks being updated.Once I opened the plugin in the second IDE i could see the task history as usual. I am not sure if I first opened a root task from the offered list, or if I already opened the subtask list and jumped straight to the subtask of the root process. However, once I opened the subtask, nothing was shown in the task history. Just a blank page. So I wanted to go back to the parent. Turned out that the subtask - after opening - was shown with
"Task #0 (Incomplete)"label once I got back to the history index. Opening a second subtask of the same parent produced the same issue. I think I end it up havingTask #x (No messages)Task #0 (Incomplete)So I stopped opening any other task as the history data was crucial to me. Then I found that in at least one subtask directory, the
ui_messages.json(UIM) was gone. In at least one other, it was set to[].I've been able to rebuild everything from the
api_conversation_history.json(ACH) using the zoo-code-history-repair tool I created (apart from the MCP Server communication which is only stored in UIM). However, I can't really explain why the"Task #0 (Incomplete)". Analysing the code, the only failure-path would be a reading error on the UIM file. Which I find weird, as it was just one single instance that was trying to open and display the task.In the wake of all this, the following values of HI got reset to
0:tokensIntokensOuttotalCostsizecacheWritescacheReadswhich obviously also ended up in the global IDX (including the truncated
taskfield)martin-rueegg commentedon Aug 16, 2026
I'm with you to first trying to stop this from happening with the current setup. I'm not sure if the approach I use in saveFile with the FileSnapshot would be a feasible approach.
But I think it is also important to think of ways to improve the way the logging is happening. In my opinion, there are several design issues:
fix(task-history): atomic per-task merge and drop shared index file (Z…