Class: Ask::Agent::CheckpointStore

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/agent/checkpoint_store.rb

Overview

Versioned, durable session checkpoints on any State::Adapter.

Every checkpoint is a full session snapshot (the same payload Session#save writes) stored under a sequential key. The adapter only needs the minimal KV contract — get, set, delete — so this works with every state provider (SQLite, Redis, Postgres, MySQL) and with custom adapters that implement nothing else. No list primitives are required.

Keys (session id + suffix):

"<id>:checkpoint:<seq>"  — one key per checkpoint
"<id>:checkpoint:head"   — the current seq (the active timeline)

Rolling back moves the head pointer; later checkpoints are kept, so a session can roll forward again (time travel). Forking copies the checkpoints up to a point into a new session id — the branch diverges from there.

Constant Summary collapse

CHECKPOINT_KEY =
":checkpoint:"
HEAD_KEY =
":checkpoint:head"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state_adapter) ⇒ CheckpointStore

Returns a new instance of CheckpointStore.

Parameters:

  • state_adapter (Ask::State::Adapter)

    backing store



29
30
31
# File 'lib/ask/agent/checkpoint_store.rb', line 29

def initialize(state_adapter)
  @state = state_adapter
end

Instance Attribute Details

#stateAsk::State::Adapter (readonly)

Returns the underlying adapter.

Returns:

  • (Ask::State::Adapter)

    the underlying adapter



34
35
36
# File 'lib/ask/agent/checkpoint_store.rb', line 34

def state
  @state
end

Instance Method Details

#checkpoint(session_id, data) ⇒ Integer

Append a checkpoint. The new checkpoint becomes the head.

Parameters:

  • session_id (String)
  • data (Hash)

    session snapshot

Returns:

  • (Integer)

    the new checkpoint seq



41
42
43
44
45
46
# File 'lib/ask/agent/checkpoint_store.rb', line 41

def checkpoint(session_id, data)
  seq = (head(session_id) || 0) + 1
  @state.set(checkpoint_key(session_id, seq), data)
  @state.set(head_key(session_id), seq)
  seq
end

#delete(session_id) ⇒ void

This method returns an undefined value.

Remove every checkpoint for a session.

Parameters:

  • session_id (String)


114
115
116
117
118
119
120
# File 'lib/ask/agent/checkpoint_store.rb', line 114

def delete(session_id)
  history(session_id).each do |seq|
    @state.delete(checkpoint_key(session_id, seq))
  end
  @state.delete(head_key(session_id))
  nil
end

#fork(session_id, at_seq: nil, new_id: SecureRandom.uuid) ⇒ String

Copy the checkpoints up to at_seq into a new session id — a branch that diverges from that point.

Parameters:

  • session_id (String)
  • at_seq (Integer, nil) (defaults to: nil)

    checkpoint to fork from; defaults to the head

  • new_id (String) (defaults to: SecureRandom.uuid)

    id for the forked session (defaults to a new uuid)

Returns:

  • (String)

    the forked session's id

Raises:

  • (ArgumentError)

    when the checkpoint does not exist



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ask/agent/checkpoint_store.rb', line 98

def fork(session_id, at_seq: nil, new_id: SecureRandom.uuid)
  at_seq ||= head(session_id)
  raise ArgumentError, "no checkpoint #{at_seq}" unless at_seq && @state.get(checkpoint_key(session_id, at_seq))

  (1..at_seq).each do |seq|
    data = @state.get(checkpoint_key(session_id, seq))
    @state.set(checkpoint_key(new_id, seq), data)
  end
  @state.set(head_key(new_id), at_seq)
  new_id
end

#head(session_id) ⇒ Integer?

Returns current head seq, or nil when the session has no checkpoints.

Parameters:

  • session_id (String)

Returns:

  • (Integer, nil)

    current head seq, or nil when the session has no checkpoints



51
52
53
# File 'lib/ask/agent/checkpoint_store.rb', line 51

def head(session_id)
  @state.get(head_key(session_id))
end

#history(session_id) ⇒ Array<Integer>

Returns all checkpoint seqs, oldest first.

Parameters:

  • session_id (String)

Returns:

  • (Array<Integer>)

    all checkpoint seqs, oldest first



57
58
59
60
# File 'lib/ask/agent/checkpoint_store.rb', line 57

def history(session_id)
  current = head(session_id)
  current ? (1..current).to_a : []
end

#load(session_id, seq: nil) ⇒ Hash?

Load a checkpoint's data.

Parameters:

  • session_id (String)
  • seq (Integer, nil) (defaults to: nil)

    checkpoint seq; defaults to the head

Returns:

  • (Hash, nil)

    the snapshot, or nil when it does not exist



67
68
69
70
71
72
# File 'lib/ask/agent/checkpoint_store.rb', line 67

def load(session_id, seq: nil)
  seq ||= head(session_id)
  return nil unless seq

  @state.get(checkpoint_key(session_id, seq))
end

#rollback(session_id, seq) ⇒ Integer

Move the head pointer to an earlier (or later) checkpoint. Later checkpoints are kept so the session can roll forward again.

Parameters:

  • session_id (String)
  • seq (Integer)

Returns:

  • (Integer)

    the seq rolled back to

Raises:

  • (ArgumentError)

    when the checkpoint does not exist



81
82
83
84
85
86
# File 'lib/ask/agent/checkpoint_store.rb', line 81

def rollback(session_id, seq)
  raise ArgumentError, "no checkpoint #{seq}" unless @state.get(checkpoint_key(session_id, seq))

  @state.set(head_key(session_id), seq)
  seq
end