Skip to content
Hydra Engine

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 toUseWhat it costs
try names, comments or types that nobody else seesan overlay116 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 trackinga fork204 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 agentsshared modeeach 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. One apply_overlay call is atomic: the first failing operation rolls back the whole call, and the error names its index.
  • Reads through it: info, functions, decompile (with the overlay's own decompilers) and contained scripts. Leave overlay out and the same client reads the shared program.
  • Keeping the work: promote_overlay replays 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. A refresh rebuilds 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 to forkCopyLimitMb (2,048 MB); above that, conflict with 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"}}
  • edit applies several operations (the five overlay kinds) in one transaction, all or none.
  • journal lists every committed change in order: who (client, name, harness), what, where, old and new value.
  • discard_edits reverts one author's unsaved renames, labels and comments, newest first, skipping values someone changed since.
  • save saves 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_overlay replays 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_edits does not undo analysis or scripts.

Checked against the engine on 2026-10-26. Every detail is in the README on GitHub.