Class: Insika::DelegationStore

Inherits:
Object
  • Object
show all
Includes:
Coercion
Defined in:
lib/insika/delegation_store.rb

Overview

Durable record of an ASYNC delegation (RFC-0010 §5, item 21 Phase 2, hermes "delegation durability"). The synchronous subagent (Phase 1) needs no record — it lives and dies inside the parent's turn. The ASYNC subagent does: the parent DISPATCHES and its turn ends; the child runs independently; when the child finishes, its result is delivered to the parent as a NEW turn (never spliced mid-turn — preserves role alternation + prompt cache). This record is what survives a kill -9 so a completed child's result is never lost.

Lifecycle (never backwards): dispatched -> completed -> delivered. dispatched the child was spawned; result not captured yet. completed the child is terminal; result/error captured durably. delivered the result was handed to the parent session (claimed — the claim is what makes delivery AT-MOST-ONCE across crashes).

Normalizes symbol->string on WRITE (the backend only round-trips JSON types), like the other domain stores.

Defined Under Namespace

Classes: Delegation

Constant Summary collapse

SCOPE =
"delegations"
KEY_PREFIX =
"delegation:"
STATUSES =
%i[dispatched completed delivered].freeze

Instance Method Summary collapse

Methods included from Coercion

blank?, deep_stringify, presence, present?, utf8

Constructor Details

#initialize(store:) ⇒ DelegationStore

Returns a new instance of DelegationStore.



37
38
39
# File 'lib/insika/delegation_store.rb', line 37

def initialize(store:)
  @store = store
end

Instance Method Details

#claim_delivery(id) ⇒ Object

completed -> delivered, ATOMICALLY (the claim) — across processes, not just fibers: the read-check-write rides Store#transaction, so two workers racing the same record serialize on the backend's lock and only one sees :completed. Returns true only for the caller that won the transition — that caller (and only it) spawns the delivery turn, so delivery is at-most-once even if the hook and recovery both fire. A record not in :completed (already delivered, or still dispatched) -> false.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/insika/delegation_store.rb', line 104

def claim_delivery(id)
  @store.transaction do
    record = @store.get(SCOPE, key_for(id))
    next false unless record && record["status"] == "completed"

    record["status"] = "delivered"
    record["updated_at"] = timestamp
    @store.set(SCOPE, key_for(id), record)
    true
  end
end

#create(parent_task_id:, parent_session_id:, parent_agent:, child_agent:, child_task_id:, child_session_id:, depth:, id: SecureRandom.uuid) ⇒ Object

-> Delegation (:dispatched).



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/insika/delegation_store.rb', line 42

def create(parent_task_id:, parent_session_id:, parent_agent:, child_agent:,
           child_task_id:, child_session_id:, depth:, id: SecureRandom.uuid)
  record = {
    "id" => id.to_s,
    "parent_task_id" => parent_task_id.to_s,
    "parent_session_id" => parent_session_id&.to_s,
    "parent_agent" => parent_agent.to_s,
    "child_agent" => child_agent.to_s,
    "child_task_id" => child_task_id.to_s,
    "child_session_id" => child_session_id.to_s,
    "depth" => depth,
    "status" => "dispatched",
    "result" => nil,
    "error" => nil,
    "created_at" => timestamp,
    "updated_at" => timestamp
  }
  @store.set(SCOPE, key_for(id), record)
  to_delegation(record)
end

#find(id) ⇒ Object

-> Delegation | nil



64
65
66
67
# File 'lib/insika/delegation_store.rb', line 64

def find(id)
  record = @store.get(SCOPE, key_for(id))
  record && to_delegation(record)
end

#find_by_child_task(child_task_id) ⇒ Object

-> Delegation | nil for a given child task (the terminal hook's lookup). O(n) scan — single-node, like PendingActionStore#open_for.



71
72
73
74
75
# File 'lib/insika/delegation_store.rb', line 71

def find_by_child_task(child_task_id)
  id = child_task_id.to_s
  scan { |d| return d if d.child_task_id == id }
  nil
end

#mark_completed(id, result: nil, error: nil) ⇒ Object

dispatched -> completed, capturing the child's result/error. Idempotent: a second call on an already-completed/delivered record is a no-op (returns the current record) — the terminal hook and recovery can race.



87
88
89
90
91
92
93
94
95
# File 'lib/insika/delegation_store.rb', line 87

def mark_completed(id, result: nil, error: nil)
  record = fetch!(id)
  return to_delegation(record) unless record["status"] == "dispatched"

  record["status"] = "completed"
  record["result"] = result
  record["error"] = error
  touch(id, record)
end

#undeliveredObject

-> [Delegation] that are NOT delivered yet (boot recovery). completed-but- undelivered = a crash between capture and delivery; dispatched = the child may or may not be terminal (the caller checks the child task).



80
81
82
# File 'lib/insika/delegation_store.rb', line 80

def undelivered
  scan.reject { |d| d.status == :delivered }
end