Class: KairosMcp::Storage::FileBackend

Inherits:
Backend
  • Object
show all
Defined in:
lib/kairos_mcp/storage/file_backend.rb

Overview

File-based storage backend (default)

This is the default storage backend for KairosChain, suitable for individual use. Data is stored in JSON/JSONL files.

Storage locations:

  • Blockchain: storage/blockchain.json
  • Action logs: skills/action_log.jsonl
  • Knowledge metadata: extracted from *.md files (no separate storage)

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Backend

create, default, load_config, register, unregister

Constructor Details

#initialize(config = {}) ⇒ FileBackend

Returns a new instance of FileBackend.



22
23
24
25
26
27
28
29
# File 'lib/kairos_mcp/storage/file_backend.rb', line 22

def initialize(config = {})
  @storage_dir = config[:storage_dir] || KairosMcp.storage_dir
  @blockchain_file = config[:blockchain_file] || KairosMcp.blockchain_path
  @action_log_file = config[:action_log_file] || KairosMcp.action_log_path

  FileUtils.mkdir_p(@storage_dir)
  FileUtils.mkdir_p(File.dirname(@action_log_file))
end

Instance Attribute Details

#action_log_fileObject (readonly)

Get the blockchain file path (for compatibility)



183
184
185
# File 'lib/kairos_mcp/storage/file_backend.rb', line 183

def action_log_file
  @action_log_file
end

#blockchain_fileObject (readonly)

Get the blockchain file path (for compatibility)



183
184
185
# File 'lib/kairos_mcp/storage/file_backend.rb', line 183

def blockchain_file
  @blockchain_file
end

#storage_dirObject (readonly)

Get the blockchain file path (for compatibility)



183
184
185
# File 'lib/kairos_mcp/storage/file_backend.rb', line 183

def storage_dir
  @storage_dir
end

Instance Method Details

#action_history(limit: 50) ⇒ Object



122
123
124
125
126
127
128
# File 'lib/kairos_mcp/storage/file_backend.rb', line 122

def action_history(limit: 50)
  return [] unless File.exist?(@action_log_file)

  File.readlines(@action_log_file)
      .last(limit)
      .filter_map { |line| JSON.parse(line, symbolize_names: true) rescue nil }
end

#all_blocksObject



98
99
100
# File 'lib/kairos_mcp/storage/file_backend.rb', line 98

def all_blocks
  load_blocks || []
end

#backend_typeObject



178
179
180
# File 'lib/kairos_mcp/storage/file_backend.rb', line 178

def backend_type
  :file
end

#clear_action_log!Object



130
131
132
133
134
135
136
# File 'lib/kairos_mcp/storage/file_backend.rb', line 130

def clear_action_log!
  File.write(@action_log_file, '')
  true
rescue StandardError => e
  warn "[FileBackend] Failed to clear action log: #{e.message}"
  false
end

#delete_knowledge_meta(_name) ⇒ Object



160
161
162
163
# File 'lib/kairos_mcp/storage/file_backend.rb', line 160

def delete_knowledge_meta(_name)
  # No-op for file backend
  true
end

#get_knowledge_meta(_name) ⇒ Object



150
151
152
153
# File 'lib/kairos_mcp/storage/file_backend.rb', line 150

def get_knowledge_meta(_name)
  # No separate metadata storage - return nil
  nil
end

#list_knowledge_metaObject



155
156
157
158
# File 'lib/kairos_mcp/storage/file_backend.rb', line 155

def list_knowledge_meta
  # No separate metadata storage - return empty array
  []
end

#load_blocksObject

Read contract (see Backend#load_blocks): nil means the ledger does not exist, and nothing else. A ledger that exists but cannot be opened or parsed — a zero-byte file included — raises Storage::Error. Returning nil there would make a damaged ledger indistinguishable from a fresh install, and the next append would rebuild from genesis over it.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kairos_mcp/storage/file_backend.rb', line 40

def load_blocks
  # Absence is decided by stat, not by File.exist?. File.exist? answers
  # false for EVERY stat(2) failure, not only ENOENT — an unsearchable
  # parent directory, a symlink loop, EIO on a network mount, or a macOS
  # ACL denying readattr all read as "no ledger here". Measured on darwin
  # 23.6: with `chmod +a "user deny readattr"` on the ledger, File.exist?
  # is false while File.read and File.write both still succeed, so the
  # ledger classified :absent and the next append rebuilt from genesis
  # over it — 6 blocks became 2. Only ENOENT and ENOTDIR mean the ledger
  # is not there; every other stat failure means we cannot tell.
  begin
    File.stat(@blockchain_file)
  rescue Errno::ENOENT, Errno::ENOTDIR
    return nil
  rescue SystemCallError => e
    raise Storage::Error,
          "cannot determine whether ledger #{@blockchain_file} exists: #{e.message}"
  end

  # FIX E — NEW CLAIM: the bytes on disk alone decide the classification;
  # the process locale never does. The ledger is always written as UTF-8,
  # but File.read without an encoding tags the bytes with the locale-derived
  # default. Started with LANG unset (launchd, cron, plain containers), a
  # ledger holding non-ASCII text then fails the parse and a healthy 705-block
  # ledger reads :corrupt (measured on a copy of a production ledger; the
  # shipped pre-fix code went further and erased it to 2 blocks on the next
  # append). The bytes were never wrong — only the read was.
  json_data = JSON.parse(File.read(@blockchain_file, encoding: Encoding::UTF_8), symbolize_names: true)
  unless json_data.is_a?(Array)
    raise Storage::Error, "ledger #{@blockchain_file} is not a JSON array (got #{json_data.class})"
  end

  json_data.map do |block_data|
    normalize_block_data(block_data)
  end
rescue Storage::Error
  raise
rescue JSON::ParserError, ArgumentError, SystemCallError, IOError, NoMethodError, TypeError => e
  raise Storage::Error, "failed to load blocks from #{@blockchain_file}: #{e.message}"
end

#ready?Boolean

===========================================================================

Utility Methods

Returns:

  • (Boolean)


174
175
176
# File 'lib/kairos_mcp/storage/file_backend.rb', line 174

def ready?
  true
end

#record_action(entry) ⇒ Object

===========================================================================

Action Log Operations



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/kairos_mcp/storage/file_backend.rb', line 106

def record_action(entry)
  normalized = {
    timestamp: entry[:timestamp] || Time.now.iso8601,
    action: entry[:action],
    skill_id: entry[:skill_id],
    details: entry[:details]
  }

  FileUtils.mkdir_p(File.dirname(@action_log_file))
  File.open(@action_log_file, 'a') { |f| f.puts(normalized.to_json) }
  true
rescue StandardError => e
  warn "[FileBackend] Failed to record action: #{e.message}"
  false
end

#save_all_blocks(blocks) ⇒ Object

Write contract (see Backend#save_all_blocks): failure raises Storage::Error. The previous false return collapsed every failure into a value that reads as benign at the call site.



90
91
92
93
94
95
96
# File 'lib/kairos_mcp/storage/file_backend.rb', line 90

def save_all_blocks(blocks)
  FileUtils.mkdir_p(File.dirname(@blockchain_file))
  File.write(@blockchain_file, JSON.pretty_generate(blocks.map { |b| block_to_hash(b) }))
  true
rescue StandardError => e
  raise Storage::Error, "failed to save blocks to #{@blockchain_file}: #{e.message}"
end

#save_block(block) ⇒ Object



81
82
83
84
85
# File 'lib/kairos_mcp/storage/file_backend.rb', line 81

def save_block(block)
  blocks = load_blocks || []
  blocks << block_to_hash(block)
  save_all_blocks(blocks)
end

#save_knowledge_meta(_name, _meta) ⇒ Object

===========================================================================

Knowledge Meta Operations

For FileBackend, metadata is not stored separately. These methods are no-ops or return empty results. The actual metadata is extracted from *.md files by KnowledgeProvider.



145
146
147
148
# File 'lib/kairos_mcp/storage/file_backend.rb', line 145

def save_knowledge_meta(_name, _meta)
  # No-op for file backend - metadata is in the files
  true
end

#update_knowledge_archived(_name, _archived, reason: nil) ⇒ Object



165
166
167
168
# File 'lib/kairos_mcp/storage/file_backend.rb', line 165

def update_knowledge_archived(_name, _archived, reason: nil)
  # No-op for file backend - archiving is folder-based
  true
end