← Back to blog

opentine v0.4.0: distinct IDs for divergent portable forks

0xcircuitbreaker··6 min read

opentine 0.4.0 fixes a subtle identity problem in portable agent-run forks: two separate decisions made from the same point should not become the same run.

Before this release, a v2 fork ID described a coordinate—the parent run and the step where history branched. Forking that coordinate twice produced the same ID even when the branch name or reason differed. Version 0.4 makes the ID describe the fork act instead.

This is an identity change, not a storage-format change. Portable *.tine artifacts remain format v2, and .tine/ repository objects remain format v3.

One fork point, many legitimate decisions

It is normal to return to one useful step and try several paths: a safer tool policy, a different prompt, or another model. Those paths share a coordinate, but they are not the same act.

A new portable fork now derives its ID from:

  • the source lineage and retained history
  • the fork point and branch
  • the caller's declared intent
  • a recorded random 128-bit nonce

The recorded basis is stored in metadata.fork, and the implementation can check that the run ID matches it. Forking the same run at the same point twice now produces two distinct run IDs and two independently addressable artifacts. The checker is an internal module in v0.4, not a stable top-level public API.

This also removes the old derived names used by replay, resume, and rerun. New IDs no longer splice artifact-controlled parent text into names such as <parent>-replay or <parent>-resume.

Signed fork provenance, without rewriting history

The top-level run ID commits to the new fork basis. When a v0.4 artifact is signed, the signature also covers metadata.fork, so changing the recorded basis invalidates the signature.

The descriptive metadata.fork_reason field remains deliberately unsigned. Version 0.3 emitted that field without including it in signatures, and v0.4 MCP flows still retain it for display; changing the rule retroactively would make a valid v0.3 signature appear broken.

That distinction matters when inspecting old provenance:

  • metadata.fork is the v0.4 identity record and is covered by new signatures.
  • metadata.fork_reason is descriptive compatibility metadata and must not be treated as a signed claim.
  • the internal fork-ID checker returns None, not False, for pre-v0.4 forks because those artifacts do not contain the new proof basis.

An old fork therefore receives no fork-ID verdict under the new identity rule. Its artifact integrity and signature can still verify independently.

Random forks, deterministic cached replay

Ordinary forks receive a random nonce because two separate interventions should have separate identities, even when every other input matches.

Cached replay has a different contract. It reuses recorded steps and creates no new execution, so it uses nonce="" and remains deterministic. Replaying the same cached path twice yields the same ID, and the existing overwrite refusal still applies.

Library callers that need reproducible fork identity can make the same choice explicitly with Run.fork(..., nonce=""). Use that only when deduplication is the intended behavior; the default random nonce represents a new intervention.

Repository identity is unchanged. V3 repository objects remain content-addressed hashes, so identical repository forks can still deduplicate.

Upgrade without migrating stored data

Install the new package normally:

python -m pip install --upgrade "opentine==0.4.0"
python -c "import opentine; print(opentine.__version__)"

Every artifact written by 0.3.0 still loads under 0.4.0 and retains its existing run ID. There is no format migration to run, and opening an old artifact does not assign it a new identity.

The breaking change is limited to the values produced for new portable forks. Code that predicted a fork ID or constructed its default filename should instead consume the value returned by the operation:

  • use the returned Run.id in Python
  • use new_run_id from the MCP fork_run result
  • pass an explicit destination with the CLI's --save
tine fork source.tine --from-step 3 --save experiment.tine

Run.fork(new_run_id=...) remains available when compatibility work must reproduce a legacy ID exactly. It should be a migration tool, not the default for new forks.

Repositories survive missing empty directories

Version 0.4 also fixes a repository portability edge case. Version control systems and archive tools do not preserve empty directories, so a committed .tine/ repository could lose empty packs/, indexes/, logs/, or refs/* namespaces and then appear damaged after checkout.

When writable, Repo.open now attempts to recreate the expected empty layout on a best-effort basis. Read-only copies can still open for reading, and fsck treats a missing packs/ directory as an empty pack store rather than corruption.

Compatibility is now a release gate

The 0.4 test suite loads golden portable artifacts and a v3 repository written by the published 0.3.0 release, then checks that the current build can load, read, and verify them. Each future release can add its own fixtures to that cross-version suite.

That is the practical contract behind this update: package behavior can improve without casually renumbering formats or rewriting stored history. Version 0.4 changes how a new fork proves that it is a distinct act while keeping the artifacts and repositories already on disk intact.

Install opentine 0.4.0 from PyPI, read the tagged 0.4.0 changelog, and review the tagged README for the complete release surface.