Class: Insika::DelegationStore
- Inherits:
-
Object
- Object
- Insika::DelegationStore
- 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
-
#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.
-
#create(parent_task_id:, parent_session_id:, parent_agent:, child_agent:, child_task_id:, child_session_id:, depth:, id: SecureRandom.uuid) ⇒ Object
-> Delegation (:dispatched).
-
#find(id) ⇒ Object
-> Delegation | nil.
-
#find_by_child_task(child_task_id) ⇒ Object
-> Delegation | nil for a given child task (the terminal hook's lookup).
-
#initialize(store:) ⇒ DelegationStore
constructor
A new instance of DelegationStore.
-
#mark_completed(id, result: nil, error: nil) ⇒ Object
dispatched -> completed, capturing the child's result/error.
-
#undelivered ⇒ Object
-> [Delegation] that are NOT delivered yet (boot recovery).
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"] = @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" => , "updated_at" => } @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 |
#undelivered ⇒ Object
-> [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 |