Binary DB v0: Workflow Records
Expand Task, Change, worktree, Line, Stash, Tag, Land, and inline workflow-link record layouts and invariants.
Audience: Core implementers and workflow integrators
These layouts are the fixed and linked records for local and remote workflow state. Read each byte layout together with the following identity, mutation, validation, and legacy-conversion rules; the record-size constant alone is not the complete contract.
Task Records#
LOCAL_TASK_RECORD_SIZE = 64
LocalTaskRecord — task.bin:
u8 task_meta
u8 local_meta
u16 payload_len
u64 payload_offset
u32 origin_plan_revision_index_plus1
u32 plan_item_index_plus1
u32 published_remote_task_index
u64 created_at_s
u64 updated_at_s
u64 plan_linked_at_s
u64 published_at_s
u64 closed_at_sREMOTE_TASK_RECORD_SIZE = 60
RemoteTaskRecord — task.bin:
u8 task_meta
u8 remote_meta
u16 payload_len
u64 payload_offset
u32 origin_plan_revision_index_plus1
u32 plan_item_index_plus1
u64 created_at_s
u64 updated_at_s
u64 plan_linked_at_s
u64 fetched_at_s
u64 closed_at_sTask identity is derived rather than stored:
task_index = record_number
task_seq = task_index + 1
task_id = derive(authority_scope, repo_namespace, "T", task_seq)Task records are append-only and must never be physically reordered. A legacy SQLite import may compact sparse legacy sequences once; it must not preserve holes by inserting padding records.
closed_at_s is a fixed tail field of LocalTaskRecord and RemoteTaskRecord, not payload and not a separate record or file. Zero means either that the effective Task state is not terminal or that an offline legacy conversion had no source close time for a terminal Task. In the latter case the Task is closed and its historical close time is unknown. A non-zero value is the close time and is valid if and only if the effective Task state is terminal under TaskRecord.task_meta and, for local authority, LocalTaskRecord.local_meta. Later publication or linkage updates may change updated_at_s but never replace the close time.
Task creation writes closed_at_s = 0. Close writes and fsyncs closed_at_s before writing and fsyncing the terminal status bits as the commit marker. Native writers never clear terminal status to reopen a Task. Readers treat a stale non-zero time on a non-terminal Task as absent, and recovery repairs it to zero. Native v0 writers must never create a terminal Task with zero close time. Because no migration-provenance bit or field exists, readers, validators, and recovery accept terminal plus zero as an unknown historical close time and do not classify that representation as corruption.
The pre-correction layout-1 local 40-byte and remote 36-byte Task records are offline conversion input only. A converter must receive the source record size explicitly and must never infer it solely from file-size divisibility. Under exclusive authority lock it rewrites the complete task.bin into a separate file using the 44-byte or 40-byte record and preserves every close time supplied by source authority. For a terminal Task whose legacy source format demonstrably did not record close time, it writes closed_at_s = 0 and must not substitute creation time, update time, conversion time, or any other inferred timestamp. A source that defines close time but supplies an invalid value still fails closed. The converter validates the entire rewritten file and atomically replaces the old file before activation. One active task.bin never mixes the pre-correction and corrected record sizes.
For bin-to-bin conversion, source planning states unplanned and explicit_unplanned both encode as TaskRecord.task_meta bit 0 clear; planned encodes as bit 0 set. This is the complete v0 planning-state encoding: v0 intentionally does not retain a third distinction between the two unplanned source spellings. Numeric Plan revision and item bindings are resolved and preserved independently. A textual plan_section_ref or plan_drift_state is accepted without another field only when it is absent or exactly derivable: section ref comes from the resolved Plan Item's heading_path_bytes, while drift state comes from the numeric binding and the current Plan/Plan-revision metadata. A non-derived or ambiguous value fails closed.
An admitted source Task status abandoned maps to TaskRecord.task_meta bit 7 canceled. It does not create another Task state field and does not infer LocalTaskRecord.local_meta bit 1 from a server source. Under the separately enumerated exact locked-source gate, the legacy source spelling canceled maps to that same existing bit and no other state. A local source explicitly carrying the independent local-abandonment state preserves that local bit under its existing semantics.
For an offline conversion from a legacy Remote Task format that demonstrably has no update or fetch time, the converter writes the corresponding updated_at_s = 0 or fetched_at_s = 0. Each zero means unknown historical time, not the conversion time. A supplied valid time is preserved exactly; a source format that defines either field but supplies an invalid value fails closed. Native v0 Remote Task writers continue to write their actual event times and do not use this legacy exception.
Change Records#
TASK_CHANGE_INDEX_RECORD_SIZE = 8
TaskChangeIndexRecord — task_change_index.bin:
u32 latest_change_index_plus1
u16 change_count
u8 next_change_ordinal
u8 reserved0LOCAL_CHANGE_RECORD_SIZE = 68
LocalChangeRecord — change.bin:
u8 change_meta
u8 local_meta
u16 payload_len
u8 change_ordinal
u8 change_state
u16 reserved1
u64 payload_offset
u32 task_index
u32 previous_change_index_plus1
u32 fork_snapshot_index_plus1
u8 published_remote_change_ordinal_plus1
u8 reserved2
u16 reserved3
u64 created_at_s
u64 updated_at_s
u64 published_at_s
u32 base_line_index_plus1
u64 archived_at_sREMOTE_CHANGE_RECORD_SIZE = 68
RemoteChangeRecord — change.bin:
u8 change_meta
u8 remote_meta
u16 payload_len
u8 change_ordinal
u8 change_state
u16 reserved1
u64 payload_offset
u32 task_index
u32 previous_change_index_plus1
u32 selected_patchset_index_plus1
u32 fork_snapshot_index_plus1
u64 created_at_s
u64 updated_at_s
u64 fetched_at_s
u32 base_line_index_plus1
u64 archived_at_sThe Change record stores neither textual Change ID nor textual Task ID. Its identity is (authority_scope, task_index, change_ordinal).
A published Local Change's Remote identity is the pair formed by its owning Local Task's published_remote_task_index and published_remote_change_ordinal_plus1 - 1. The Task field selects the exact Remote Task ordinal; the Change field selects C-01..C-64 inside that Task. No Local Change stores or requires a global Remote change_index.
For a Remote Change, ChangePatchsetIndexRecord.latest_patchset_index_plus1 is the latest/current Patchset and selected_patchset_index_plus1 is the sole persisted mutable selected-Patchset authority used by review, readiness, and Land. Selection is Change state, not an immutable Patchset property. Zero means no selected Patchset. A non-zero selected pointer must reference a live Patchset owned by this Change. Source current_patchset_number resolves through the latest index; source selected_patchset_number resolves through the selected pointer. They may differ and must never be collapsed into one value. Selection updates this existing pointer without mutating a Patchset record. This is a semantic rename of the existing four-byte slot and does not shift that slot or any field in the 44-byte prefix. The later inline lifecycle extension appends only the two declared tail fields.
For the admitted legacy server format only, a landed Change that has one or more surviving Patchsets may supply current_patchset_number = 0 even though latest/current remains rebuildable. Conversion accepts that zero only when the source selected pointer is non-zero and resolves to the surviving Patchset owned by the Change with the greatest patch_ordinal; it writes that Patchset as ChangePatchsetIndexRecord.latest_patchset_index_plus1. Any other zero, missing, ambiguous, non-owned, omitted, or disagreeing latest evidence fails closed. The selected pointer remains independently stored and is not inferred from this exception.
ChangeRecord.change_state bit 0 is canceled; bits 1 through 7 are reserved and zero. The canceled bit is valid only with archived lifecycle. A source Change status abandoned maps to archived lifecycle, canceled set, and the existing superseded bit clear. It does not create another Change record or payload. For a legacy Remote Change format that demonstrably has no fetch time, conversion writes fetched_at_s = 0; a supplied valid value is preserved, and an invalid supplied value fails closed.
published_remote_change_ordinal_plus1 = 0 means absent. Values 1..64 encode Remote Change ordinals 0..63, so C-01 is stored as 1 and C-64 as 64; values 65..255 are invalid. reserved2 and reserved3 must be zero. LocalChangeRecord.local_meta bit 0 is clear if and only if the ordinal field is zero. When it is set, the ordinal field and published_at_s are non-zero, and the owning Local Task must also have its published bit set. The Remote ordinal may differ from the Local change_ordinal; publication stores the exact Task-scoped ordinal returned by the Remote authority.
An offline converter reading the legacy explicit textual published_change_id parses the exact C-01..C-64 suffix and writes its ordinal-plus-one value directly; it never fabricates a global Remote Change index. A converter reading the former fixed u32 published_remote_change_index interpretation must explicitly select that source schema and resolve the referenced Remote record to its owning Task and Task-scoped ordinal before writing this field. Missing, mismatched, or ambiguous publication authority fails closed.
Inline Change Lifecycle Fields#
base_line_index_plus1 and archived_at_s are required fixed fields in each 68-byte Local or Remote Change record. They are not payload, a separate record, or a separate file. base_line_index_plus1 is required and references the exact LineRecord from which the Change was created. A Change never stores a duplicate base-Line name or textual Line ID. If fork_snapshot_index_plus1 is non-zero, it references the exact live Snapshot observed as that base Line's head when the Change was created. A Snapshot's line_index_plus1 is its immutable authoring-Line identity, not Line-head membership; it may differ from base_line_index_plus1 after a Snapshot authored on another Line becomes the base Line's head and must not be rewritten or compared for equality. The public forked_from_line value is derived from the numeric base-Line identity.
archived_at_s is non-zero if and only if the Change lifecycle is archived, except that an archived Change converted from a legacy source format that demonstrably did not record archive time retains zero as unknown historical archive time. ChangeRecord.change_meta bit 7 distinguishes a superseded Change inside that archived lifecycle, while ChangeRecord.change_state bit 0 distinguishes cancellation. Archive and reopen each rewrite and durably commit one complete 68-byte Change record; lifecycle bits and archived_at_s change together, with no side-file ordering or intra-record status-last protocol. Recovery rejects a non-archived Change carrying a non-zero archive time. Native v0 archive writes require a non-zero archive time. Because no migration bit is added, readers, validators, and recovery accept archived plus zero as unknown legacy time. The public review state is active lifecycle plus review_pending; landed_at is derived from the successful Land record's updated_at_s and is not stored again on the Change.
Change creation resolves the normalized base-Line name by exact payload comparison, observes that Line's current head as the optional fork Snapshot, then appends the complete Change commit record. Recovery rejects a zero or missing base Line, a missing/ambiguous Line, a missing or tombstoned fork Snapshot, a lifecycle/time mismatch, or an invalid superseded bit for a committed Change. Recovery does not compare the fork Snapshot's authoring Line with the Change's base Line.
The immediately preceding active layout-1 representation with a 44-byte change.bin and ordinal-aligned 8-byte change_lifecycle.bin is offline conversion input only. A converter must receive that source representation explicitly, require both files to have the same record count, preserve each Change's complete 44-byte prefix, append the exact paired base-Line index and archive time, validate the resulting complete 52-byte file, and omit change_lifecycle.bin from the activated target. It must not infer a missing base Line or align rows by any textual identity.
An earlier pre-side-file layout-1 generation is also offline conversion input only when its declared source schema independently supplies the exact base_line. Conversion resolves that value to an authority-local line_index, requires any supplied forked_from_line to equal that base Line, and preserves the exact referenced live fork Snapshot without comparing or rewriting its authoring Line. It preserves a supplied valid source archive time and writes archived_at_s = 0 only when the admitted source format demonstrably has no archive-time field. An invalid supplied time or any unavailable or ambiguous required non-time value fails closed before materializing the complete 68-byte Change file.
Worktree Cursor#
WORKTREE_CURSOR_RECORD_SIZE = 28
WorktreeCursorRecord — .ait-worktree/cursor.bin:
u8 cursor_meta
u8 reserved0
u16 reserved1
u32 task_index
u32 selected_change_index_plus1
u32 pending_pre_land_target_snapshot_index_plus1
u32 pending_landed_snapshot_index_plus1
u64 updated_at_sThe cursor is local-only, disposable scratch state. It is not workflow history and must be excluded from content snapshots.
Line And Stash Records#
LINE_RECORD_SIZE = 40
LineRecord — line.bin:
u8 line_meta
u8 reserved0
u16 line_name_len
u64 line_name_offset
u32 head_snapshot_index_plus1
u64 created_at_s
u64 updated_at_s
u64 archived_at_sSTASH_RECORD_SIZE = 8
StashRecord — stash.bin:
u8 stash_meta
u8 reserved0
u16 reserved1
u32 stash_snapshot_indexStash storage is local-only. stash_snapshot_index must reference a content snapshot whose kind is stash. Stash deletion tombstones the record and never shifts later indexes.
Local Tag Records#
TAG_RECORD_SIZE = 24
TagRecord — tag.bin:
u8 tag_meta
u8 reserved0
u16 payload_len
u64 payload_offset
u32 snapshot_index
u64 created_at_stag_index is the stable internal Tag identity and is the zero-based ordinal of TagRecord in tag.bin. The public Tag identity remains its normalized, non-empty UTF-8 name. snapshot_index references a live content Snapshot.
Tag creation appends its typed payload before its fixed record. Force-replacing an existing name preserves tag_index, appends a new payload, then updates the fixed record; an interrupted replacement may leave only an unreferenced payload. Deletion sets the tombstone bit and never deletes the referenced Snapshot or shifts a Tag ordinal. Re-creation of the same name clears the tombstone only while preserving the same tag_index.
Land Records#
TASK_LAND_INDEX_RECORD_SIZE = 8
TaskLandIndexRecord — task_land_index.bin:
u32 latest_land_index_plus1
u16 land_count
u16 reserved0
CHANGE_LAND_INDEX_RECORD_SIZE = 8
ChangeLandIndexRecord — change_land_index.bin:
u32 latest_land_index_plus1
u16 land_count
u8 next_land_ordinal
u8 reserved0LOCAL_LAND_RECORD_SIZE = 44
LocalLandRecord — land.bin:
u8 land_meta
u8 land_ordinal
u8 change_ordinal
u8 failure_kind
u32 change_index
u32 previous_task_land_index_plus1
u32 previous_change_land_index_plus1
u32 pre_land_target_snapshot_index_plus1
u32 landed_snapshot_index_plus1
u64 submitted_at_s
u64 updated_at_s
u32 target_line_index_plus1SERVER_LAND_RECORD_SIZE = 48
ServerLandRecord — land.bin:
u8 land_meta
u8 land_ordinal
u8 change_ordinal
u8 failure_kind
u32 change_index
u32 patchset_index
u32 previous_task_land_index_plus1
u32 previous_change_land_index_plus1
u32 pre_land_target_snapshot_index_plus1
u32 landed_snapshot_index_plus1
u64 submitted_at_s
u64 updated_at_s
u32 target_line_index_plus1Local Land authority is the stored snapshot pair. Server Land authority also stores the exact accepted Patchset. Land status updates that keep one L-## use status-last two-phase writes.
land_ordinal is unique in (change_index, land_ordinal). Values 0..63 render as L-01..L-64 under the full owning Change identity. A Server Land's patchset_index is the immutable Patchset accepted by that attempt; it does not make Land identity Patchset-scoped. One Change may therefore retain L-01 against one Patchset and L-02 against a later Patchset. ChangeLandIndexRecord owns next-ordinal allocation and its latest/previous links follow Change-scoped Land ordinal. TaskLandIndexRecord and previous_task_land_index_plus1 retain physical Task inventory only.
Land mode is stored in land_meta bits 5 through 6. Source direct, merge, and ff-only map to the fixed encodings below; an unknown mode fails closed. The source result object does not become payload. Target-Line identity and the pre-land/landed Snapshot values come from the fixed Land record; a blocked result's blocker_class maps to failure_kind. BASE_STALE and POLICY_BLOCKED map to base_stale and policy_blocked respectively. Any unrecognized blocker or result value that is not exactly derivable from that fixed record fails closed.
Native v0 Land submission requires non-zero submitted_at_s. For an offline legacy conversion only, a landed Change whose source authority records the exact landed result but never recorded submission time may materialize its first and only successful Land with land_ordinal = 0, both previous-Land links zero, submitted_at_s = 0, and updated_at_s equal to the preserved source landed_at. Zero means unknown historical submission time; a converter must not substitute Change creation/update time or conversion time. This rule does not permit reconstruction when target Line, landed Snapshot, accepted Patchset where required, or landed time is missing or ambiguous, and native v0 writers must never create a new Land with zero submission time.
Inline Land Target Line Authority#
The owning Land record's target_line_index_plus1 is required and references the exact target LineRecord selected when the Land is submitted. It is fixed schema, not payload. A Land never stores a duplicate target-Line name or textual Line ID.
When present, pre_land_target_snapshot_index_plus1 references the exact live Snapshot observed as the target Line's head before its update, while landed_snapshot_index_plus1 references the exact live Snapshot installed as that Line's new head. A referenced Snapshot's line_index_plus1 remains its immutable authoring-Line identity and may differ from target_line_index_plus1; Land never relabels it or compares those Line indexes for equality. A landed Change's public target_line and landed_at values derive from its successful Land's target Line and updated_at_s; queued, running, blocked, failed, canceled, and updating Land attempts retain their target Line even when neither Snapshot is yet present.
The admitted legacy server format may retain more than one succeeded Land for one Change and may append a blocked, failed, canceled, or updating attempt after an earlier success. Conversion preserves every Land row and its exact accepted Patchset. If any surviving Land succeeded, the Change lifecycle is normalized to landed regardless of the duplicated source Change status. The succeeded Land with the greatest land_ordinal supplies the public target Line and landed time; later non-succeeded attempts remain immutable history and do not undo the completed land. A selected/current Patchset created after that success remains independent mutable Change authority and need not equal the accepted Patchset. A source Change that claims landed but has no provable successful Land follows only the explicit reconstruction or named-omission rules below; it never receives a fabricated Land implicitly.
Legacy Change landed_at is a non-authoritative duplicated projection when exact Land authority exists. Conversion validates the supplied value as a timestamp but does not require it to equal any Land time and does not store it; the authoritative public time is the latest succeeded Land's updated_at_s. Likewise, the Change's mutable selected-Patchset pointer and a historical Land's exact accepted Patchset are independent authorities after submission. Conversion preserves both and does not require them to remain equal.
Land submission resolves the normalized Line name by exact payload comparison and appends and fsyncs one complete Land commit record containing the required target-Line reference. Recovery rejects a zero/missing target Line, a missing or tombstoned referenced Snapshot, or a status/Snapshot-presence disagreement for a committed Land. Recovery does not compare a referenced Snapshot's authoring Line with the Land target Line.
The immediately preceding layout-1 representation with 32-byte Local or 36-byte Server Land records plus ordinal-aligned 4-byte land_target_line.bin is offline conversion input only. Conversion requires matching headers and exact record counts, appends each exact target-Line value to its unchanged Land prefix, validates every widened record, omits the retired side file, and fails closed on zero, missing, extra, misaligned, or ambiguous input. The source files are not modified.
An earlier pre-side-file layout-1 generation without land_target_line.bin is also offline conversion input only. Conversion resolves the exact source target_line to an authority-local line_index, preserves each exact stored live Snapshot reference without comparing or rewriting its authoring Line, and fails closed if the target or a referenced Snapshot is unavailable or ambiguous before materializing the complete widened Land record.