Class: Insika::Retention

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/retention.rb

Overview

WS8 (phase 2): retention — the engine forgets what is old enough to forget. Age-based purge of the CONVERSATION footprint: sessions (and their per-session tool/context traces), terminal tasks (and their checkpoints), customer/tenant memory cells and outcome records. The knob is data, not code: settings.retention_days (Integer; nil/0 = OFF — parity, nothing is ever swept by default).

Cadence: the Tick calls run every pass; an internal DAILY claim (the tick's window idiom — one key, a timestamp, 24 h) makes the actual sweep once a day, so the O(n) scans never ride the 60 s loop. A sweep that crashes propagates to the tick, which logs and keeps ticking.

Constant Summary collapse

SCOPE =
"retention"
KEY =
"claim"
BUDGET_KEY =
"budget_claim"
MEMORY_TTL_KEY =

the MEMORY TTL's own daily claim. Deliberately NOT the age-based KEY — memory TTLs sweep on their own knob (memory_ttl_days), gated by neither retention_days nor the age-based claim (D5).

"memory_ttl_claim"
ARTIFACT_TTL_KEY =

the ARTIFACT TTL's own daily claim — same discipline as the memory TTL: reports expire on their own knob (artifact_ttl_days), never gated by retention_days (a deployment that keeps conversations forever must still expire the PII-carrying reports).

"artifact_ttl_claim"
WINDOW =

one sweep per day, at most

86_400
TERMINAL =
%w[completed failed cancelled].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_store:, task_store:, checkpoint_store:, memory_store:, outcome_store:, tool_trace_store: nil, context_trace_store: nil, outbox_store: nil, shadow_pair_store: nil, settings_store: nil, budget_ledger: nil, funnel_store: nil, followup_store: nil, contact_store: nil, proposal_store: nil, model_visible_trace_store: nil, store:, window: WINDOW, now: nil, harvest_store: nil, artifact_store: nil) ⇒ Retention

Returns a new instance of Retention.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/insika/retention.rb', line 34

def initialize(session_store:, task_store:, checkpoint_store:,
               memory_store:, outcome_store:, tool_trace_store: nil,
               context_trace_store: nil, outbox_store: nil, shadow_pair_store: nil,
                settings_store: nil, budget_ledger: nil, funnel_store: nil,
                followup_store: nil, contact_store: nil, proposal_store: nil,
                model_visible_trace_store: nil,
                 store:, window: WINDOW, now: nil, harvest_store: nil,
                 artifact_store: nil)
  @session_store = session_store
  @task_store = task_store
  @checkpoint_store = checkpoint_store
  @memory_store = memory_store
  @outcome_store = outcome_store
  @tool_trace_store = tool_trace_store
  @context_trace_store = context_trace_store
  @model_visible_trace_store = model_visible_trace_store #  ; nil = parity
  @outbox_store = outbox_store
  @shadow_pair_store = shadow_pair_store
  @settings_store = settings_store
  @budget_ledger = budget_ledger # WS2 counter GC; nil = nothing to sweep
  @funnel_store = funnel_store   #  ; nil = nothing to sweep
  @followup_store = followup_store #  ; nil = nothing to sweep
  @contact_store = contact_store   #  ; nil = nothing to sweep
  @proposal_store = proposal_store #  ; nil = nothing to sweep
  @store = store
  @window = window
  @now = now # injectable for specs (a deterministic "today")
  @harvest_store = harvest_store #  ; nil = nothing to sweep
  @artifact_store = artifact_store #  ; nil = nothing to sweep
end

Instance Attribute Details

#memory_storeObject (readonly)

the sweep reads the memory store's cells/records (specs seed facts through it).



67
68
69
# File 'lib/insika/retention.rb', line 67

def memory_store
  @memory_store
end

Instance Method Details

#runObject

-> { claimed: false } | { claimed: true, sessions:, tasks:, outcomes:, memory:, deliveries: }. Either shape may carry budget_cells: — the budget counter GC is NOT gated by retention_days (see #sweep_budget_cells). Either shape may carry memory_ttl: — the memory TTL sweep, gated by ITS OWN daily claim and knob, never by retention_days (see #sweep_memory_ttl).



75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
# File 'lib/insika/retention.rb', line 75

def run
  budget_cells = sweep_budget_cells
  memory_ttl = sweep_memory_ttl
  artifacts = sweep_artifacts
  days = retention_days
  unless days.to_i.positive? && claim_window
    summary = { claimed: false }
    summary[:budget_cells] = budget_cells if budget_cells
    summary[:memory_ttl] = memory_ttl if memory_ttl
    summary[:artifacts] = artifacts if artifacts
    return summary
  end

  cutoff = now - (days.to_i * 86_400)
  summary = { claimed: true, sessions: sweep_sessions(cutoff),
              tasks: sweep_tasks(cutoff), outcomes: sweep_outcomes(cutoff),
              memory: @memory_store.prune_older_than(cutoff),
              deliveries: sweep_outbox(cutoff),
              pairs: sweep_shadow_pairs(cutoff) }
  summary[:funnel] = sweep_funnel(cutoff) if @funnel_store
  # the follow-up footprint ages out with the rest — records
  # and contact cells under the SAME retention_days gate (nil collaborator
  # = nothing to sweep, base graph parity).
  summary[:followups] = @followup_store.delete_older_than(cutoff) if @followup_store
  summary[:contacts] = @contact_store.delete_older_than(cutoff) if @contact_store
  # proposals are evidence OF a transcript — when the
  # transcript dies, the proposal's excerpt is gone and the pending fact is
  # stale. Pending AND terminal rows age out together; a proposal is
  # re-derivable (D2), so pruning is never data loss. The session MARKERS
  # are never pruned (the store's rule — the marker is the claim).
  summary[:proposals] = @proposal_store.delete_older_than(cutoff) if @proposal_store
  # candidates (pending AND terminal), log rows and
  # snapshots are DERIVED data of transcripts (D11) — when the transcripts
  # die, the candidates' excerpts are gone; they are re-derivable (D2), so
  # pruning is never data loss. The session markers are never pruned.
  summary[:harvest] = @harvest_store.delete_older_than(cutoff) if @harvest_store
  summary[:budget_cells] = budget_cells if budget_cells
  summary[:memory_ttl] = memory_ttl if memory_ttl
  summary[:artifacts] = artifacts if artifacts
  summary
end