Several writers
When two agents want to change one program, Hydra Engine offers a private overlay, a copy-on-write fork, or shared writing with an author journal.
On this page
A writable program has one owner by default: the client that opened it writable. Any other client that tries to rename, comment, analyse or save it gets not_owner, and that answer already offers the ways out, each as a ready request (overlaySuggestion, suggestion). on main · 12.3-hydra.2
Pick one
| The agent wants to | Use | What it costs |
|---|---|---|
| try names, comments or types that nobody else sees | an overlay | 116 ms to open and about 54 MB of heap on a 243,855-function program; nothing on disk |
| work long or heavily: re-analysis, bulk types, version tracking | a fork | 204 to 225 ms and 2.8 to 5.0 MB for a 1.49 GB project on ReFS; 8.5 s and 1.56 GB as a full copy to NTFS |
| annotate one program together with other agents | shared mode | each change is its own short transaction under the program's lock |
The overlay and fork figures are from the engine's README: Windows 11, JDK 25, a Dev Drive (ReFS), the shared program already open and warm; medians of 3 runs for the overlay, ranges of 3 runs for the fork.
Overlays: private annotations
An overlay is a client's own program object over the stored file of a program others read, plus a log of what the client did to it. Nobody else sees it, and it is never saved.
key = "p1:/target.exe"
s.call("open_program", path=key, overlay="hyp-1")
s.call("apply_overlay", program=key, overlay="hyp-1", ops=[
{"op": "rename", "address": "rva:0x67bc40", "name": "update_state"},
{"op": "comment", "address": "rva:0x67bc40", "type": "plate", "text": "guess: called once per tick"},
{"op": "signature", "address": "rva:0x67bc40", "signature": "void update_state(void *self, float dt)"},
])
print(s.call("decompile", program=key, overlay="hyp-1", address="rva:0x67bc40")["c"])- Five operations:
rename,label,comment,signature,data_type. Oneapply_overlaycall is atomic: the first failing operation rolls back the whole call, and the error names itsindex. - Reads through it:
info,functions,decompile(with the overlay's own decompilers) and containedscripts. Leaveoverlayout and the same client reads the shared program. - Keeping the work:
promote_overlayreplays the log into a writable copy, either a project the client holds writable or a new fork, checks it is the same build by SHA-256, and saves it. - Lifetime:
drop_overlay, closing the program or the project, or the client disconnecting drops it. Arefreshrebuilds it from its log on the new revision.
Forks: a working copy
fork_project copies a project into a folder of the caller's and opens it writable, while every other client keeps reading the original:
f = s.call("fork_project", project="p1", path=r"F:\work\agent-b")Abridged answer, from a small project on a Dev Drive:
{"project": "p4", "method": "clone", "cloned": true, "bytes": 2294844, "copyMs": 67,
"baseRevision": "5129f36b6257c921", "sidecar": "F:\\work\\agent-b\\Demo.fork.json"}On Windows 11 24H2 and later the copy is a block clone on ReFS: the fork shares the original's blocks until either side writes. Elsewhere it is a full copy. mode decides what happens when cloning is not possible:
auto(default): clone, or a full copy up toforkCopyLimitMb(2,048 MB); above that,conflictwith the cost so the caller can decide.copy-on-write: clone or refuse.full: copy whatever it costs.
The fork records its source and baseRevision in <name>.fork.json next to its .gpr, and a fork of a fork keeps the whole lineage. Compare the original's revision with the fork's baseRevision to know whether the original moved on since the fork was made.
A source that someone is writing is refused, because its saved state could change under the copy. The answer names the writers in holders.
Shared mode: several writers, one program
Every writer opens the program with "writable": true, "share": true:
s.call("open_program", path="choice.exe", writable=True, share=True)
s.call("rename", address="0x140001010", name="parse_args", expect="FUN_140001010")Each change is committed on its own, one at a time, under the program's exclusive lock, and every writer sees it at once. The last writer wins per value. To catch a lost update instead, pass expect, the value you read. Two agents renaming the same function, the second one late:
{"code": "conflict", "details": {"reason": "changed", "current": "parse_args", "suggest": "reread"}}editapplies several operations (the five overlay kinds) in one transaction, all or none.journallists every committed change in order: who (client,name,harness), what, where, old and new value.discard_editsreverts one author's unsaved renames, labels and comments, newest first, skipping values someone changed since.savesaves everyone's changes. The last writer to close with unsaved changes must save or pass"discard": true.
A writer that dies loses only its uncommitted change. If it was the last writer and changes are unsaved, the program stays open, writer-less, until someone joins and saves or discards; only a daemon shutdown throws that work away, and it is logged.
The engine's tests run 4 clients with 2 threads each making 3,200 renames and comments with 64 saves on a small program, in 6.0 s, with no lost or duplicated edit and the journal complete.
Limits
- An overlay shows the program as it was stored when the overlay opened, not other writers' unsaved changes. Bulk type imports, structure creation, analysis and scripts that must keep their changes need a fork.
- An overlay costs heap: one program object each. For long work on one large program, a fork costs disk instead.
promote_overlayreplays by address. It does not merge with other changes in the target.- Forks are block clones only on ReFS. Btrfs, XFS and APFS forks count as full copies.
- The journal lives in the daemon's memory while the program is open, up to 200,000 entries, and is not saved with the program.
discard_editsdoes not undo analysis or scripts.
Checked against the engine on 2026-10-26. Every detail is in the README on GitHub.