Clock Extension — Normative Specification

Version: 1.3 Status: Active Depends: ENTITY-CORE-PROTOCOL.md (v7.3+)


Path notation. Paths in this document use peer-relative notation (without leading /{peer_id}/). All peer-relative paths resolve to the local peer's namespace: system/tree means /{local_peer_id}/system/tree. Every path in the entity tree is absolute at rest — rooted at a peer identity. See ENTITY-CORE-PROTOCOL.md §1.4 for the path model. Cross-peer examples use absolute paths with explicit peer identities.

1. Overview

The clock extension provides a unified timing mechanism for the entity system. It formalizes the system_clock_ms() function used by the history extension (EXTENSION-HISTORY.md §5.1, §5.2) and the revision extension (EXTENSION-REVISION.md §6.1), and layers additional clock disciplines on top: logical clocks for causal ordering within a peer, vector clocks for concurrency detection across peers, and hybrid logical clocks (HLC) for total ordering correlated with real time.

1.1 Scope

This extension covers:

This extension does not cover:

1.2 Relationship to Other Extensions

History extension. History transitions record a timestamp field using system_clock_ms(). This extension formalizes that function. History does not change — it continues to call system_clock_ms(), which this extension defines as returning a system/clock/timestamp value. When the clock extension is present, transitions MAY additionally record the logical clock value for causal ordering.

Revision extension. Revision entries record a timestamp field using system_clock_ms(). During sync, clock state is merged alongside version DAGs (§5). The lww merge strategy (EXTENSION-REVISION.md §2.3) compares version timestamps — with HLC, this comparison becomes causally meaningful rather than wall-clock dependent.

Subscription extension. Tick subscriptions (§3.3) deliver periodic clock events through the subscription extension's notification mechanism. Tick events are tree change events on system/clock/tick/ paths, firing standard subscription notifications.

1.3 Design Principles

Wall-clock is the baseline. Every peer has a wall clock. system_clock_ms() works without the clock extension installed — it's a host function. The clock extension formalizes it and layers causal clocks on top.

Opt-in complexity. A peer that only needs timestamps uses wall-clock mode (the default). A peer that needs causal ordering enables logical clocks. A peer that needs concurrency detection enables vector clocks. A peer that needs total ordering enables HLC. Each level adds cost — no peer pays for what it doesn't use.

Clock is local state. Each peer maintains its own clock. There is no global clock. Cross-peer ordering emerges from clock merge during sync, not from clock synchronization.


2. Type Definitions

2.1 Timestamp

Wall-clock milliseconds since Unix epoch. This is what system_clock_ms() returns.

system/clock/timestamp := {
  fields: {
    ms: {type_ref: "primitive/uint"}
         ; Milliseconds since the Unix epoch
  }
}

system_clock_ms() is formally defined as:

system_clock_ms():
  return host_wall_clock_ms()
  ; Returns the host system's wall-clock time as milliseconds since Unix epoch.
  ; The value is a primitive/uint suitable for use in timestamp fields.
  ; Monotonicity is not guaranteed — the host clock may be adjusted.

The value returned by system_clock_ms() is the ms field of a system/clock/timestamp entity. When existing extensions write timestamp: system_clock_ms(), they are writing this value.

2.2 Logical Clock

Lamport-style scalar counter. Increments on each tree write. Provides a total order within a single peer and a partial causal order across peers (when merged during sync).

system/clock/logical := {
  fields: {
    counter: {type_ref: "primitive/uint"}
              ; Monotonically increasing event counter.
              ; Increments on every tree write.
  }
}

2.3 Vector Clock

Peer-indexed counters for concurrency detection. Each entry tracks the last known counter for a peer. Element-wise max merge during sync.

system/clock/vector := {
  fields: {
    entries: {map_of: {type_ref: "primitive/uint"}}
              ; peer_id (string) → counter (uint).
              ; Each peer increments its own entry on tree writes.
              ; Merge takes element-wise max.
  }
}

Two vector clocks can be compared to detect concurrency: if neither dominates the other (some entries higher in each), the events are concurrent.

2.4 Hybrid Logical Clock

Combines physical time with a logical counter. Provides total order correlated with real time. O(1) size regardless of peer count.

system/clock/hlc := {
  fields: {
    physical: {type_ref: "primitive/uint"}
               ; Physical component (ms since epoch).
               ; Always ≥ the local wall clock at time of creation.
    logical:  {type_ref: "primitive/uint"}
               ; Logical component. Disambiguates events with the same physical time.
               ; Resets to 0 when physical advances.
    peer:     {type_ref: "system/hash"}
               ; Content hash of the peer's identity entity.
               ; Breaks ties when physical and logical are equal.
  }
}

2.5 Clock Configuration

Peer-level clock configuration:

system/clock/config := {
  fields: {
    mode:          {type_ref: "primitive/string"}
                    ; "wall" | "logical" | "vector" | "hlc"
                    ; Determines which clock type is maintained.
                    ; Default: "wall"
    wall_clock:    {type_ref: "primitive/bool", optional: true}
                    ; Whether to include wall-clock timestamps alongside
                    ; logical/vector/hlc clocks. Default: true.
                    ; When mode is "wall", this field is ignored (always true).
    tick_interval: {type_ref: "primitive/uint", optional: true}
                    ; Milliseconds between periodic tick events.
                    ; Absent = no periodic ticks.
  }
}

Stored at system/clock/config.

2.6 Clock State

Current clock state for the peer:

system/clock/state := {
  fields: {
    mode:      {type_ref: "primitive/string"}
                ; Active clock mode
    timestamp: {type_ref: "system/clock/timestamp", optional: true}
                ; Current wall-clock timestamp (present when wall_clock is true)
    logical:   {type_ref: "system/clock/logical", optional: true}
                ; Current logical clock (present when mode is "logical", "vector", or "hlc")
    vector:    {type_ref: "system/clock/vector", optional: true}
                ; Current vector clock (present when mode is "vector")
    hlc:       {type_ref: "system/clock/hlc", optional: true}
                ; Current HLC (present when mode is "hlc")
  }
}

2.7 Compare Types

system/clock/compare-params := {
  fields: {
    a: {type_ref: "primitive/any"}
        ; First clock value (timestamp, logical, vector, or hlc)
    b: {type_ref: "primitive/any"}
        ; Second clock value (same type as a)
  }
}

system/clock/compare-result := {
  fields: {
    order: {type_ref: "primitive/string"}
            ; "before" | "after" | "concurrent" | "equal"
            ; "before" = a happened before b
            ; "after"  = a happened after b
            ; "concurrent" = a and b are concurrent (vector clocks only)
            ; "equal"  = a and b represent the same clock value
  }
}

2.8 Tick Event

system/clock/tick := {
  fields: {
    sequence: {type_ref: "primitive/uint"}
               ; Monotonically increasing tick counter
    state:    {type_ref: "system/clock/state"}
               ; Clock state at tick time
  }
}

Tick entities are written to system/clock/tick/latest at each tick interval. The subscription extension fires notifications for subscribers watching this path.


3. Handler

3.1 Handler Manifest

system/handler := {
  data: {
    pattern:    "system/clock"
    name:       "clock"
    operations: {
      now:     {output_type: "system/clock/state"}
      compare: {input_type: "system/clock/compare-params", output_type: "system/clock/compare-result"}
      tick:    {input_type: "system/subscription/request"}
    }
  }
}

Handler entity at pattern path system/clock. Index entry at system/handler/system/clock. Handler grant at system/capability/grants/system/clock.

3.2 now

Returns the current clock state. Reads the peer's clock without advancing it.

EXECUTE system/clock  operation: "now"
  resource: {targets: ["system/clock"]}
handle_now(ctx):
  config = tree.get("system/clock/config") or default_config()
  state = read_current_clock_state(config)
  return {type: "system/clock/state", data: state}
read_current_clock_state(config):
  state = {mode: config.data.mode}

  if config.data.mode == "wall" or config.data.wall_clock != false:
    state.timestamp = {ms: system_clock_ms()}

  if config.data.mode == "logical":
    state.logical = tree.get("system/clock/logical") or {counter: 0}

  if config.data.mode == "vector":
    state.logical = tree.get("system/clock/logical") or {counter: 0}
    state.vector = tree.get("system/clock/vector") or {entries: {}}

  if config.data.mode == "hlc":
    state.logical = tree.get("system/clock/logical") or {counter: 0}
    state.hlc = tree.get("system/clock/hlc") or {physical: system_clock_ms(), logical: 0, peer: local_peer_id}

  return state

3.3 compare

Compare two clock values. Both values MUST be the same clock type.

EXECUTE system/clock  operation: "compare"
  resource: {targets: ["system/clock"]}
  params: {
    type: "system/clock/compare-params"
    data: {
      a: {ms: 1709000000000}
      b: {ms: 1709000001000}
    }
  }
handle_compare(ctx, params):
  a = params.data.a
  b = params.data.b

  order = compare_clocks(a, b)
  return {type: "system/clock/compare-result", data: {order: order}}

Comparison algorithms are defined in §6.

3.4 tick

Creates a subscription for periodic clock events. This is a convenience operation that creates a subscription on the system/clock/tick/latest path through the subscription extension.

EXECUTE system/clock  operation: "tick"
  resource: {targets: ["system/clock/tick/*"]}
  params: {
    type: "system/subscription/request"
    data: {
      deliver_to:     {uri: "system/inbox/clock-watcher"}
      deliver_token:  <hash>
    }
  }

The clock handler delegates to the subscription handler, subscribing to system/clock/tick/latest. The tick interval is determined by the peer's clock configuration (§2.5).


4. Emit Integration

4.1 Clock as Emit Pathway Consumer

The clock extension integrates with the emit pathway — the same integration point used by the history extension (EXTENSION-HISTORY.md §5.1) and the subscription extension (EXTENSION-SUBSCRIPTION.md §4.1).

Clock's position in the emit pathway consumer ordering (first emitting consumer, after query indexes) is specified in SYSTEM-COMPOSITION.md §2.2. Clock persistence frequency options (every-emit, periodic, tick-only) are discussed in SYSTEM-COMPOSITION.md §6.2.

The clock field on the execution context is an extension-contributed context field per SYSTEM-COMPOSITION.md §1.5. CLOCK is the owning extension: it registers the clock field at peer initialization (type system/clock/state; §2.6), advances the field only during its own consumer position (SYSTEM-COMPOSITION.md §2.2 position 2), and is the authoritative source for current clock state during a cascade. Downstream consumers (HISTORY, optionally COMPUTE and SUBSCRIPTION) read ctx.clock by name; access subfields by mode (e.g., ctx.clock.logical.counter, ctx.clock.vector.entries, ctx.clock.hlc). Consumers MUST handle the null case (CLOCK not installed) and the absent-subfield case (CLOCK in a mode that doesn't populate the requested subfield).

On each tree write, the emit pathway advances the clock before other consumers (history, subscriptions) run:

emit_entity(path, new_entity, execution_context):
  ; --- Clock advancement (this extension) ---
  if is_clock_engine_path(path):
    ; Clock engine output — do not advance (prevents infinite recursion)
  else:
    advance_clock(execution_context)

  ; --- Normal emit pathway continues ---
  ; Content store put, tree binding update, history recording,
  ; subscription notification — as defined by core protocol and other extensions.
  ; execution_context.clock is available to downstream consumers.

is_clock_engine_path(path):
  ; Guard only engine-written paths, not the entire system/clock/ namespace.
  ; Config paths (system/clock/config) are handler-written and SHOULD advance the clock.
  return path starts with "system/clock/logical"
      or path starts with "system/clock/vector"
      or path starts with "system/clock/hlc"

4.2 Clock Advancement Algorithm

advance_clock(execution_context):
  config = tree.get("system/clock/config") or default_config()
  mode = config.data.mode

  ; Build a system/clock/state value for this cascade step.
  ; Subfields populated per mode; mode field always set.
  state = {mode: mode}

  if mode == "wall":
    state.timestamp = {ms: system_clock_ms()}
    execution_context.clock = state
    return

  ; --- Logical clock (used by all non-wall modes) ---
  current_logical = tree.get("system/clock/logical") or {counter: 0}
  new_counter = current_logical.counter + 1
  new_logical = {type: "system/clock/logical", data: {counter: new_counter}}
  tree.set("system/clock/logical", content_store.put(new_logical))
  state.logical = {counter: new_counter}

  if mode == "vector":
    ; --- Vector clock ---
    current_vector = tree.get("system/clock/vector") or {entries: {}}
    new_entries = copy(current_vector.entries)
    new_entries[local_peer_id] = new_counter
    new_vector = {type: "system/clock/vector", data: {entries: new_entries}}
    tree.set("system/clock/vector", content_store.put(new_vector))
    state.vector = {entries: new_entries}

  if mode == "hlc":
    ; --- Hybrid logical clock ---
    current_hlc = tree.get("system/clock/hlc") or {physical: 0, logical: 0, peer: local_peer_id}
    new_hlc = hlc_local_event(current_hlc)
    tree.set("system/clock/hlc", content_store.put({type: "system/clock/hlc", data: new_hlc}))
    state.hlc = new_hlc

  if config.data.wall_clock != false:
    state.timestamp = {ms: system_clock_ms()}

  execution_context.clock = state

4.3 Clock State Writes Do Not Advance the Clock

Writing to clock engine-output paths — the persistence paths (system/clock/logical, system/clock/vector, system/clock/hlc) and the scheduled tick paths (system/clock/tick/*) — MUST NOT trigger clock advancement. Engine output is produced by the clock consumer itself during emit processing; advancing on it would recurse (persistence) or, for a periodic tick, would advance the clock at wall rate on an idle peer — so in logical/vector/hlc mode the counter would count wall time instead of causal events and every causal comparison (§6.4) silently degrades. The rule is general: a write the clock engine itself emits does not advance the clock. (Tick-path exclusion added from the core-go tick-emission validation, 2026-07-22; §4.3's original enumeration predated tick emission.)

Configuration paths (system/clock/config) are handler-written via explicit EXECUTE and do not create recursion risk. Configuration changes SHOULD advance the clock like any other tree mutation. See SYSTEM-COMPOSITION.md §6.1 for the engine-output vs configuration path distinction.


5. Sync Integration

5.1 Clock Exchange During Sync

During sync (EXTENSION-REVISION.md §7.1), peers exchange clock state alongside version heads. The clock merge happens after version fetch and before version integration.

sync_clocks(local_clock_state, remote_clock_state, mode):
  if mode == "logical":
    merge_logical(local_clock_state.logical, remote_clock_state.logical)
  elif mode == "vector":
    merge_logical(local_clock_state.logical, remote_clock_state.logical)
    merge_vector(local_clock_state.vector, remote_clock_state.vector)
  elif mode == "hlc":
    merge_logical(local_clock_state.logical, remote_clock_state.logical)
    merge_hlc(local_clock_state.hlc, remote_clock_state.hlc)

5.2 Logical Clock Merge

merge_logical(local, remote):
  merged_counter = max(local.counter, remote.counter)
  new_logical = {type: "system/clock/logical", data: {counter: merged_counter}}
  tree.set("system/clock/logical", content_store.put(new_logical))

After merge, the next local event increments from the merged value, ensuring that post-sync events have a counter greater than all events from both peers.

5.3 Vector Clock Merge

merge_vector(local, remote):
  merged_entries = copy(local.entries)
  for (peer_id, counter) in remote.entries:
    if peer_id not in merged_entries or counter > merged_entries[peer_id]:
      merged_entries[peer_id] = counter
  new_vector = {type: "system/clock/vector", data: {entries: merged_entries}}
  tree.set("system/clock/vector", content_store.put(new_vector))

5.4 HLC Merge

merge_hlc(local, remote):
  ; Standard HLC receive algorithm (Kulkarni et al.)
  wall = system_clock_ms()
  new_physical = max(wall, local.physical, remote.physical)

  if new_physical == local.physical and new_physical == remote.physical:
    new_logical = max(local.logical, remote.logical) + 1
  elif new_physical == local.physical:
    new_logical = local.logical + 1
  elif new_physical == remote.physical:
    new_logical = remote.logical + 1
  else:
    new_logical = 0    ; physical advanced past both — reset logical

  new_hlc = {
    type: "system/clock/hlc"
    data: {physical: new_physical, logical: new_logical, peer: local_peer_id}
  }
  tree.set("system/clock/hlc", content_store.put(new_hlc))

5.5 Version Clock Field

Version entities (EXTENSION-REVISION.md §2.1) MAY include the clock value at commit time. This is an optional field on system/revision/entry via open types (ENTITY-CORE-PROTOCOL.md §2.7):

clock: {type_ref: "primitive/any", optional: true}

Extends: system/revision/entry

When present: Contains the clock value (system/clock/logical, system/clock/vector, or system/clock/hlc) at the time the version was created. Enables causally-aware version comparison — the lww merge strategy (EXTENSION-REVISION.md §2.3) can use the clock field instead of the wall-clock timestamp field.

When absent: Version ordering uses the timestamp field (wall-clock). This is the default behavior without the clock extension.


6. Clock Algorithms

6.1 Lamport Increment

lamport_increment(current):
  return {counter: current.counter + 1}

6.2 HLC Local Event

Advance the HLC for a local event (tree write):

hlc_local_event(current_hlc):
  wall = system_clock_ms()
  new_physical = max(wall, current_hlc.physical)

  if new_physical == current_hlc.physical:
    new_logical = current_hlc.logical + 1
  else:
    new_logical = 0    ; physical advanced — reset logical

  return {physical: new_physical, logical: new_logical, peer: local_peer_id}

6.3 HLC Receive

Advance the HLC on receiving a remote clock value (sync merge). Defined in §5.4.

6.4 Clock Comparison

6.4.1 Timestamp Comparison

compare_timestamps(a, b):
  if a.ms < b.ms: return "before"
  if a.ms > b.ms: return "after"
  return "equal"

6.4.2 Logical Clock Comparison

compare_logical(a, b):
  if a.counter < b.counter: return "before"
  if a.counter > b.counter: return "after"
  return "equal"

6.4.3 Vector Clock Comparison

Standard vector clock partial order: a ≤ b iff ∀i: a[i] ≤ b[i]. Strict ordering: a < b iff a ≤ b and a ≠ b. When neither a ≤ b nor b ≤ a, the events are concurrent.

compare_vector(a, b):
  all_peers = union(keys(a.entries), keys(b.entries))
  a_leq_b = true
  b_leq_a = true
  equal = true

  for peer_id in all_peers:
    a_val = a.entries.get(peer_id) or 0
    b_val = b.entries.get(peer_id) or 0
    if a_val > b_val:
      a_leq_b = false
      equal = false
    if b_val > a_val:
      b_leq_a = false
      equal = false

  if equal: return "equal"
  if a_leq_b: return "before"     ; a happened before b
  if b_leq_a: return "after"      ; a happened after b
  return "concurrent"              ; neither dominates

6.4.4 HLC Comparison

compare_hlc(a, b):
  if a.physical < b.physical: return "before"
  if a.physical > b.physical: return "after"
  if a.logical < b.logical: return "before"
  if a.logical > b.logical: return "after"
  ; Physical and logical equal — compare peer identity for total order
  if a.peer < b.peer: return "before"
  if a.peer > b.peer: return "after"
  return "equal"

HLC comparison produces a total order — never "concurrent". The peer identity tiebreaker is deterministic but arbitrary.


7. Security Considerations

7.1 Clock State Access

Clock state at system/clock/* is local peer state. Access follows standard capability scoping — the clock handler grant controls who can read clock values via the now operation. Clock state entities in the tree are accessible to any capability that covers the system/clock/ prefix.

7.2 Wall-Clock Trust

Wall-clock timestamps are untrusted across peers. A peer can report any value for system_clock_ms(). This is an acknowledged limitation, not something the clock extension solves. Defenses:

7.3 Vector Clock Growth

Vector clock entries grow with the number of peers that have written to the tree. For systems with many transient peers, the vector clock may grow unboundedly. Implementations SHOULD prune entries for peers that are no longer active (no writes for a configurable period). Pruning loses ordering information for pruned peers but bounds storage cost.

The MAX_VECTOR_ENTRIES constant (§8) provides a hard bound. When the limit is reached, the oldest entry (lowest counter) is evicted.

7.4 Tick Subscriptions

Tick subscriptions respect the subscription extension's capability model (EXTENSION-SUBSCRIPTION.md §9). A tick subscription requires capability for the system/clock handler with the tick operation and a valid deliver token for the inbox URI.


8. Constants

ConstantValueDescription
DEFAULT_TICK_INTERVAL_MS1000Default tick interval in milliseconds
MAX_VECTOR_ENTRIES1024Maximum entries in a vector clock before eviction
MAX_HLC_DRIFT_MS60000Maximum allowed HLC physical drift from wall clock (ms)
DEFAULT_CLOCK_MODE"wall"Default clock mode when no configuration is present

9. Write Authorization

Clock handler operations (now, compare, tick) are read-only — no tree writes.

Clock advancement (§3.3) is an autonomous background operation. Writes to system/clock/* paths are authorized by the clock handler's own grant. The local peer identity is the author.

Tick paths are history-excluded by default. The scheduled tick paths (system/clock/tick/*) MUST NOT be history-recorded by default — a periodic tick otherwise generates history at wall rate forever on an idle peer (unbounded growth by omission, the same class as chain-error-marker retention). The .../latest pointer plus the monotonic sequence (§2.8) give every consumer what they need; the historical tick series carries no value. A deployment MAY opt a tick path back into history recording, but only under a bounded retention policy. This mirrors EXTENSION-HISTORY.md §3.2's exclusion of system/history/* and §4.3's exclusion of engine output from advancement — one rule: engine-output ephemera are excluded from the recording/advancement feedback loops. (From the core-go tick-emission validation, 2026-07-22.)

The other system/clock/* paths (advancement persistence, config) are not history-excluded: the recursion prevention in EXTENSION-HISTORY.md §3.2 covers only system/history/*, and an advancement write is recorded (with the clock handler grant as the authorizing capability) only when something else is already writing — bounded by write activity, not wall time.

See ENTITY-CORE-PROTOCOL.md §6.8 for the general write authorization model.


10. Conformance

10.1 MUST Implement

10.2 SHOULD Implement

10.3 MAY Implement

10.4 Implementation-Defined

10.5 Types Installed

system/clock/timestamp
system/clock/logical
system/clock/vector
system/clock/hlc
system/clock/config
system/clock/state
system/clock/compare-params
system/clock/compare-result
system/clock/tick

10.6 Handler Registered

system/handler/system/clock := system/handler {
  pattern:    "system/clock"
  name:       "clock"
  operations: {now, compare, tick}
  ; See §3.1 for full operation-spec map with input/output types
}