Class: KairosMcp::KairosChain::Chain

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/kairos_chain/chain.rb

Overview

INV-D: an append may only add a block to the end of the sequence that is on disk at that moment. The in-memory sequence never replaces the one on disk. When the sequence on disk cannot be determined, nothing is appended. INV-E: the ledger's state is carried by one entrance that every reader passes through (#load_state), not by the block sequence. INV-G: under a backend that does not declare its contract, Chain refuses to append and reads the ledger as unreadable.

Constant Summary collapse

LOAD_STATES =

The five values #load_state can take. Only :readable and :absent permit an append; only :readable makes #chain / #latest_block meaningful.

%i[absent unreadable empty corrupt readable].freeze
APPEND_FLAG =

Re-entrancy flag. Thread-level (not fiber-level) on purpose: an append may cross fibers, and a nested append would deadlock on its own flock.

:kairos_chain_append_in_progress
EMPTY_CHAIN =
[].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(chain_file: nil, storage_backend: nil) ⇒ Chain

Constructing the default backend can itself fail — a malformed config scalar, a data directory that cannot be created on a read-only or full mount. That construction is therefore deferred into the protected region of classify_disk; doing it here would put it outside every rescue and break "Chain.new never raises".

Parameters:

  • chain_file (String, nil) (defaults to: nil)

    dead argument, kept for call-site compatibility

  • storage_backend (Storage::Backend, nil) (defaults to: nil)

    storage backend to use



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 54

def initialize(chain_file: nil, storage_backend: nil)
  # FIX B — NEW CLAIM: Chain.new resolves no paths of its own, so it
  # returns an object for every input. KairosMcp.blockchain_path here ran
  # outside every rescue and raised for an unresolvable data directory
  # (KAIROS_DATA_DIR='~nosuchuser/…' → ArgumentError; deleted CWD →
  # Errno::ENOENT from Dir.pwd — both measured). @chain_file is dead
  # (assigned, never read); the argument is kept for call-site
  # compatibility only. Path resolution now happens solely inside
  # classify_disk's protected region, where it degrades to :unreadable.
  @chain_file = chain_file
  @storage_backend = storage_backend
  @load_state, @chain = classify_disk
end

Instance Attribute Details

#load_stateObject (readonly)

Returns the value of attribute load_state.



45
46
47
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 45

def load_state
  @load_state
end

Instance Method Details

#add_block(data) ⇒ Block

Append one block to whatever is on disk right now.

Returns:

  • (Block)

    the appended block

Raises:



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 89

def add_block(data)
  # ⓪ flag first, before the lock: a nested append would deadlock on its own
  #    flock. The path question is asked here too, so a backend that answers
  #    a relative path is refused before any file is created.
  if Thread.current.thread_variable_get(APPEND_FLAG)
    raise ChainStateError.new(
      'append already in progress on this thread (nested append is forbidden)',
      state: @load_state
    )
  end
  path = ledger_path

  Thread.current.thread_variable_set(APPEND_FLAG, true)
  begin
    # ① take the key (a file beside the ledger, never the ledger itself)
    with_lock(path) do
      # ② read disk and classify
      state, disk_blocks = classify_disk

      # ③ refuse anything that is neither readable nor absent
      unless %i[readable absent].include?(state)
        raise ChainStateError.new("cannot append: ledger is #{state}", state: state)
      end

      # ④ the base is the disk sequence, or a genesis-only sequence
      base = state == :readable ? disk_blocks : [Block.genesis]

      # ⑤ build on the base's tail and write base + new
      new_block = build_block(base.last, data)
      appended = base + [new_block]
      storage_backend.save_all_blocks(appended.map(&:to_h))

      # ⑥ only a successful write advances this instance's sequence and state
      @chain = appended
      @load_state = :readable

      new_block
    end
  ensure
    # ⑦ lower the flag — reached only by the call that raised it, because ⓪
    #    raises before this begin block
    Thread.current.thread_variable_set(APPEND_FLAG, nil)
  end
end

#chainObject

The block sequence, but only when the ledger was readable. Every other state yields a frozen empty array: callers must consult #load_state first.



70
71
72
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 70

def chain
  @load_state == :readable ? @chain : EMPTY_CHAIN
end

#latest_blockObject



74
75
76
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 74

def latest_block
  @load_state == :readable ? @chain.last : nil
end

#storage_typeSymbol

Returns :file, :sqlite, ... or :unavailable when the backend could not be constructed.

Returns:

  • (Symbol)

    :file, :sqlite, ... or :unavailable when the backend could not be constructed



136
137
138
139
140
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 136

def storage_type
  storage_backend.backend_type
rescue StandardError
  :unavailable
end

#valid?Boolean

An alias for "the ledger was readable". Not a separate integrity pass: the four predicates of §3 already ran during classification.

Returns:

  • (Boolean)


80
81
82
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 80

def valid?
  @load_state == :readable
end