insika 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -17
- data/README.md +7 -1
- data/bin/insika +211 -8
- data/docs/AGENTS.md +377 -2
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +99 -9
- data/docs/CONTEXT.md +127 -16
- data/docs/DEPLOY.md +5 -2
- data/docs/EVALS.md +19 -0
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +14 -8
- data/docs/OBSERVABILITY.md +54 -0
- data/docs/REFINEMENT.md +3 -3
- data/docs/RELEASING.md +27 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SECURITY.md +61 -0
- data/docs/SKILLS.md +2 -2
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +1 -1
- data/lib/insika/agent_profile.rb +145 -10
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +118 -13
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +84 -1
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +3 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +716 -6
- data/lib/insika/dsl/runtime.rb +9 -3
- data/lib/insika/dsl/server_boot.rb +70 -1
- data/lib/insika/dsl.rb +83 -3
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +9 -2
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/pairwise.rb +18 -6
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +687 -70
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +1 -1
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +6 -1
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +6 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +22 -1
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +144 -4
- data/lib/insika/server/rack_app.rb +16 -0
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +4 -0
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +875 -14
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +22 -22
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +262 -5
- data/lib/insika/studio/nav_icons.rb +6 -1
- data/lib/insika/studio/views/agent_detail.erb +627 -84
- data/lib/insika/studio/views/agents.erb +10 -6
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +56 -1
- data/lib/insika/studio/views/settings.erb +2 -0
- data/lib/insika/studio/views/skills.erb +0 -1
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/task_store.rb +21 -1
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +25 -1
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_state.rb +38 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +300 -13
- data/lib/insika.rb +92 -3
- metadata +77 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<header class="page-head">
|
|
2
|
+
<div>
|
|
3
|
+
<nav class="crumbs"><span>harvest</span></nav>
|
|
4
|
+
<h1>Harvest<span class="sub"><%= @agent ? "#{@agent}" : "no agent" %></span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<% if insika[:harvest_store].nil? %>
|
|
9
|
+
<div class="empty">No harvest — the store is not wired.</div>
|
|
10
|
+
<% elsif @agents.empty? %>
|
|
11
|
+
<div class="empty">No agents yet. Add a <span class="mono">harvest:</span> block to an agent's pack to mine its finished traffic.</div>
|
|
12
|
+
<% else %>
|
|
13
|
+
<div class="card">
|
|
14
|
+
<form method="post" action="/studio/harvest" class="actions">
|
|
15
|
+
<%== csrf_tag %>
|
|
16
|
+
<label>
|
|
17
|
+
Agent
|
|
18
|
+
<select name="agent">
|
|
19
|
+
<% @agents.each do |id| %>
|
|
20
|
+
<option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
</label>
|
|
24
|
+
<label title="Ignore the mined markers and re-read the whole window (the ledger dedup still protects).">
|
|
25
|
+
<input type="checkbox" name="full" value="1"> full window
|
|
26
|
+
</label>
|
|
27
|
+
<button type="submit" class="primary">Mine now</button>
|
|
28
|
+
</form>
|
|
29
|
+
<div class="skill-desc muted">
|
|
30
|
+
Reads this agent's finished conversations, asks the configured miner for SKILL
|
|
31
|
+
proposals, and filters them through the negative list and the evidence ledger.
|
|
32
|
+
Nothing is ever applied automatically — a candidate only lands after the double
|
|
33
|
+
gate AND a human approval.
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if @agents.size > 1 %>
|
|
38
|
+
<div class="filter-bar">
|
|
39
|
+
<% @agents.each do |id| %>
|
|
40
|
+
<a class="btn" href="/studio/harvest?agent=<%= Rack::Utils.escape(id) %>"<%== ' aria-current="page"' if id == @agent %>><%= id %></a>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<%# The human's inbox. %>
|
|
46
|
+
<% if @candidates.any? %>
|
|
47
|
+
<h2>Awaiting approval<span class="sub">the human's answer</span></h2>
|
|
48
|
+
<% @candidates.each do |cand| %>
|
|
49
|
+
<div class="card">
|
|
50
|
+
<div class="skill-desc">
|
|
51
|
+
<span class="pill ok">gated</span>
|
|
52
|
+
<span class="mono"><%= cand.name %></span>
|
|
53
|
+
<span class="muted">· <%= cand.description %></span>
|
|
54
|
+
</div>
|
|
55
|
+
<% Array(cand.triggers).each do |t| %>
|
|
56
|
+
<span class="pill"><%= t %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<div class="skill-desc muted">
|
|
59
|
+
store <span class="mono"><%= cand.agent %></span> · proposed by
|
|
60
|
+
<span class="mono"><%= cand.proposer %></span> · <%= (cand.eval_gate || {})["passed_cases"] %>/<%= (cand.eval_gate || {})["cases"] %> case(s) no regression
|
|
61
|
+
· conversion <%= (cand.conversion_gate || {})["current"] %> vs <%= (cand.conversion_gate || {})["baseline"] %> (threshold <%= (cand.conversion_gate || {})["threshold"] %>)
|
|
62
|
+
</div>
|
|
63
|
+
<% if (excerpt = candidate_excerpt(cand)).any? %>
|
|
64
|
+
<details class="skill-desc">
|
|
65
|
+
<summary>evidence excerpt</summary>
|
|
66
|
+
<% excerpt.each do |sid, idx, text| %>
|
|
67
|
+
<div class="preview muted">
|
|
68
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a> [<%= idx %>]: <%= text %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</details>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% Array(cand.origin).each do |sid| %>
|
|
74
|
+
<a class="mono muted" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a>
|
|
75
|
+
<% end %>
|
|
76
|
+
<form method="post" action="/studio/harvest/promote" class="actions">
|
|
77
|
+
<%== csrf_tag %>
|
|
78
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
79
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
80
|
+
<label>
|
|
81
|
+
Note
|
|
82
|
+
<input type="text" name="note" placeholder="why (optional)">
|
|
83
|
+
</label>
|
|
84
|
+
<button type="submit" class="primary">Promote</button>
|
|
85
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
86
|
+
</form>
|
|
87
|
+
<div class="skill-desc muted">
|
|
88
|
+
Promoting snapshots the pre-promotion state, writes the skill into this store's
|
|
89
|
+
scope, enables it on the allowlist and appends the promotion log row. Rollback
|
|
90
|
+
is the one-click mirror.
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% end %>
|
|
95
|
+
|
|
96
|
+
<%# Gated-but-blocked: the ruler's hole, named. %>
|
|
97
|
+
<% if @blocked.any? %>
|
|
98
|
+
<h2>Blocked<span class="sub">gated, waiting on the ruler</span></h2>
|
|
99
|
+
<div class="card">
|
|
100
|
+
<ul class="history">
|
|
101
|
+
<% @blocked.each do |cand| %>
|
|
102
|
+
<li>
|
|
103
|
+
<span class="pill err">blocked</span>
|
|
104
|
+
<span class="mono"><%= cand.name %></span>
|
|
105
|
+
<span class="preview muted">
|
|
106
|
+
<% reason = (cand.conversion_gate || {})["reason"] %>
|
|
107
|
+
<% if reason == "no_frozen_baseline" %>
|
|
108
|
+
the store has no frozen funnel baseline — freeze it first on the
|
|
109
|
+
<a href="/studio/funnel">Funnel page</a>
|
|
110
|
+
<% elsif reason %>
|
|
111
|
+
<%= reason %>
|
|
112
|
+
<% else %>
|
|
113
|
+
<%= (cand.eval_gate || {})["reason"] %>
|
|
114
|
+
<% end %>
|
|
115
|
+
</span>
|
|
116
|
+
</li>
|
|
117
|
+
<% end %>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
|
|
122
|
+
<%# Mined, not yet gated. %>
|
|
123
|
+
<% if @pending_gates.any? %>
|
|
124
|
+
<h2>Pending<span class="sub">mined, awaiting the double gate</span></h2>
|
|
125
|
+
<div class="card">
|
|
126
|
+
<ul class="history">
|
|
127
|
+
<% @pending_gates.each do |cand| %>
|
|
128
|
+
<li>
|
|
129
|
+
<span class="pill">pending</span>
|
|
130
|
+
<span class="mono"><%= cand.name %></span>
|
|
131
|
+
<span class="preview muted"><%= cand.description %></span>
|
|
132
|
+
<form method="post" action="/studio/harvest/gate" class="actions inline">
|
|
133
|
+
<%== csrf_tag %>
|
|
134
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
135
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
136
|
+
<button type="submit" class="primary">Gate it</button>
|
|
137
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
138
|
+
</form>
|
|
139
|
+
</li>
|
|
140
|
+
<% end %>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
<% end %>
|
|
144
|
+
|
|
145
|
+
<%# The promotion log — the single ledger, with the rollback mirror. %>
|
|
146
|
+
<% if @promotions.any? %>
|
|
147
|
+
<h2>Promoted<span class="sub">the append-only log</span></h2>
|
|
148
|
+
<div class="card">
|
|
149
|
+
<ul class="history">
|
|
150
|
+
<% @promotions.each do |row| %>
|
|
151
|
+
<li>
|
|
152
|
+
<% if row.rolled_back_at %>
|
|
153
|
+
<span class="pill err">rolled back</span>
|
|
154
|
+
<% else %>
|
|
155
|
+
<span class="pill ok">promoted</span>
|
|
156
|
+
<% end %>
|
|
157
|
+
<span class="mono"><%= row.skill %></span>
|
|
158
|
+
<span class="preview muted">
|
|
159
|
+
<%= row.approver %> · <%= row.at %>
|
|
160
|
+
<% if row.rolled_back_at %>· rolled back <%= row.rolled_back_at %><% end %>
|
|
161
|
+
</span>
|
|
162
|
+
<% unless row.rolled_back_at %>
|
|
163
|
+
<form method="post" action="/studio/harvest/rollback" class="actions inline">
|
|
164
|
+
<%== csrf_tag %>
|
|
165
|
+
<input type="hidden" name="agent" value="<%= row.agent %>">
|
|
166
|
+
<input type="hidden" name="snapshot_ref" value="<%= row.snapshot_ref %>">
|
|
167
|
+
<input type="text" name="reason" placeholder="why (optional)">
|
|
168
|
+
<button type="submit" class="ghost">Rollback</button>
|
|
169
|
+
</form>
|
|
170
|
+
<% end %>
|
|
171
|
+
</li>
|
|
172
|
+
<% end %>
|
|
173
|
+
</ul>
|
|
174
|
+
</div>
|
|
175
|
+
<% end %>
|
|
176
|
+
|
|
177
|
+
<%# The negative list block. %>
|
|
178
|
+
<% if @negative && @negative.rules.any? %>
|
|
179
|
+
<h2>Negative list<span class="sub">the versioned seed, engine-applied</span></h2>
|
|
180
|
+
<div class="card">
|
|
181
|
+
<ul class="history">
|
|
182
|
+
<% @negative.rules.each do |rule| %>
|
|
183
|
+
<li>
|
|
184
|
+
<span class="pill"><%= rule.rule %></span>
|
|
185
|
+
<span class="preview muted"><%= rule.pattern %><%= " — #{rule.note}" if presence(rule.note) %></span>
|
|
186
|
+
<span class="preview muted">rejected: <%= negative_rule_counts[rule.rule] || 0 %></span>
|
|
187
|
+
</li>
|
|
188
|
+
<% end %>
|
|
189
|
+
</ul>
|
|
190
|
+
<div class="skill-desc muted">
|
|
191
|
+
The list is pack data on the profile (hot-editable); the versioned
|
|
192
|
+
rules file seeds it via
|
|
193
|
+
<span class="mono">insika harvest:negative import --agent ID --file F</span>.
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
|
|
198
|
+
<%# The ruler block. %>
|
|
199
|
+
<% if @criterion %>
|
|
200
|
+
<h2>The conversion criterion<span class="sub">frozen before the first promotion</span></h2>
|
|
201
|
+
<div class="card">
|
|
202
|
+
<div class="skill-desc">
|
|
203
|
+
<span class="pill">metric <%= @criterion.rule.metric %></span>
|
|
204
|
+
<span class="pill">window <%= @criterion.rule.window %></span>
|
|
205
|
+
<span class="pill">not worse than <%= @criterion.rule.threshold %></span>
|
|
206
|
+
<span class="mono muted"><%= @criterion.sha %></span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="skill-desc muted">
|
|
209
|
+
The ruler is the frozen criterion file, read-only here; the
|
|
210
|
+
store's frozen baseline lives on the <a href="/studio/funnel">Funnel page</a>. A
|
|
211
|
+
store without one can mine, but the gate parks its candidates.
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
215
|
+
|
|
216
|
+
<% if @runs.any? %>
|
|
217
|
+
<h2>Runs<span class="sub">most recent first</span></h2>
|
|
218
|
+
<div class="card">
|
|
219
|
+
<ul class="history">
|
|
220
|
+
<% @runs.each do |run| %>
|
|
221
|
+
<li>
|
|
222
|
+
<span class="pill <%= run.status == 'failed' ? 'err' : 'ok' %>"><%= run.status %></span>
|
|
223
|
+
<span class="mono"><%= run.id[0, 12] %></span>
|
|
224
|
+
<span class="preview muted">
|
|
225
|
+
<%= run.candidates %> candidate(s) ·
|
|
226
|
+
<% if run.cost && run.cost["spent"].to_i.positive? %><%= run.cost["spent"] %> tokens · <% end %>
|
|
227
|
+
<%= run.started_at %>
|
|
228
|
+
</span>
|
|
229
|
+
</li>
|
|
230
|
+
<% end %>
|
|
231
|
+
</ul>
|
|
232
|
+
</div>
|
|
233
|
+
<% end %>
|
|
234
|
+
<% end %>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<h1>Overview<span class="sub">at a glance</span></h1>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="head-actions">
|
|
9
|
+
<%== agent_filter_form("/studio/home", @agent) %>
|
|
9
10
|
<span class="pill <%= @active_now.positive? ? "ok online" : "info" %> plain"><%= @active_now %> active now</span>
|
|
10
11
|
<a class="btn primary" href="/studio/playground">Open playground →</a>
|
|
11
12
|
</div>
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
<% @recent.each do |s| %>
|
|
59
60
|
<li>
|
|
60
61
|
<a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
|
|
61
|
-
<span class="avatar sm"><%= s.id[0, 2].upcase %></span>
|
|
62
|
+
<span class="avatar sm <%= avatar_class(s.id) %>"><%= s.id[0, 2].upcase %></span>
|
|
62
63
|
<span class="convo-main">
|
|
63
64
|
<span class="convo-id mono"><%= s.id[0, 20] %></span>
|
|
64
65
|
<span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="csp-nonce" content="<%= csp_nonce %>">
|
|
7
7
|
<title>Insika Studio</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="<%= asset_path("favicon.svg") %>">
|
|
8
9
|
<link rel="stylesheet" href="<%= asset_path("application.css") %>">
|
|
9
10
|
<script type="module" src="<%= asset_path("application.js") %>" defer></script>
|
|
10
11
|
</head>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<header class="page-head">
|
|
2
|
+
<div>
|
|
3
|
+
<nav class="crumbs"><span>operate</span></nav>
|
|
4
|
+
<h1>Parity<span class="sub">the shadow experiment</span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="head-actions">
|
|
7
|
+
<%== agent_filter_form("/studio/parity", @agent) %>
|
|
8
|
+
</div>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<% if @criterion.nil? %>
|
|
12
|
+
<div class="empty">
|
|
13
|
+
No shadow channel is registered. Set <span class="mono">INSIKA_RELAY_SHADOW=1</span> and
|
|
14
|
+
restart — this page appears only when a channel is actually running in shadow.
|
|
15
|
+
</div>
|
|
16
|
+
<% elsif @pairs.empty? %>
|
|
17
|
+
<div class="empty">
|
|
18
|
+
No pairs yet. A mirrored exchange records a pair the moment our turn ends; the
|
|
19
|
+
incumbent's half lands when the mirror reports its reply
|
|
20
|
+
(<span class="mono">POST /channels/relay/shadow-reply</span>).
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<% r = @report %>
|
|
24
|
+
<div class="card">
|
|
25
|
+
<div class="skill-desc">
|
|
26
|
+
<span class="pill <%= { pass: 'ok', fail: 'err', insufficient: 'warn', invalid: 'err' }.fetch(r.verdict, 'info') %>"><%= r.verdict %></span>
|
|
27
|
+
<span class="muted"><%= r.reason %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<% sha_check = r.checks.find { |c| c.id == :criterion_sha } %>
|
|
30
|
+
<div class="skill-desc">
|
|
31
|
+
<span class="mono"><%= r.criterion_sha %></span>
|
|
32
|
+
<% if sha_check&.met %>
|
|
33
|
+
<span class="pill ok">frozen · matches every pair in the window</span>
|
|
34
|
+
<% else %>
|
|
35
|
+
<span class="pill err">NOT VERIFIED — the window's pairs do not all carry this frozen hash</span>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<h2>Checks<span class="sub">the arithmetic, not the conclusion</span></h2>
|
|
41
|
+
<div class="card">
|
|
42
|
+
<table class="grid">
|
|
43
|
+
<thead><tr><th>check</th><th>required</th><th>actual</th><th></th></tr></thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<% r.checks.each do |c| %>
|
|
46
|
+
<tr>
|
|
47
|
+
<td class="mono"><%= c.id %></td>
|
|
48
|
+
<td><%= c.required %></td>
|
|
49
|
+
<td><% if c.actual.is_a?(Hash) %><%= c.actual.map { |k, v| "#{k}: #{v}" }.join(" · ") %><% else %><%= c.actual %><% end %></td>
|
|
50
|
+
<td><span class="pill <%= c.met ? 'ok' : 'err' %>"><%= c.met ? "met" : "no" %></span></td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<h2>Pairs / day<span class="sub">floor <%= @criterion.rule.pairs_per_day %> · a gap day invalidates the window</span></h2>
|
|
58
|
+
<div class="card">
|
|
59
|
+
<table class="grid">
|
|
60
|
+
<thead><tr><th>date</th><th>pairs</th><th>decided</th><th></th></tr></thead>
|
|
61
|
+
<tbody>
|
|
62
|
+
<% r.daily.each do |d| %>
|
|
63
|
+
<tr>
|
|
64
|
+
<td class="mono"><%= d[:date] %></td>
|
|
65
|
+
<td><%= d[:pairs] %></td>
|
|
66
|
+
<td><%= d[:decided] %></td>
|
|
67
|
+
<td><% if d[:floor].zero? %><span class="muted">partial day — reported, not required</span><% elsif d[:pairs] >= d[:floor] %><span class="pill ok">met</span><% else %><span class="pill err">below floor</span><% end %></td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% end %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<% if r.counts %>
|
|
75
|
+
<h2>Per store<span class="sub">a store blocks its own cut even when the aggregate passes</span></h2>
|
|
76
|
+
<div class="card">
|
|
77
|
+
<table class="grid">
|
|
78
|
+
<thead><tr><th>store</th><th>decided</th><th>win-or-tie</th><th>worse</th><th>meets floor</th></tr></thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
<% r.per_agent.each do |agent, stats| %>
|
|
81
|
+
<tr>
|
|
82
|
+
<td class="mono"><%= agent %></td>
|
|
83
|
+
<td><%= stats[:decided] %></td>
|
|
84
|
+
<td><%= stats[:win_or_tie] %></td>
|
|
85
|
+
<td><%= stats[:worse] %></td>
|
|
86
|
+
<td><% if stats[:meets].nil? %><span class="muted">below per-store minimum</span><% elsif stats[:meets] %><span class="pill ok">yes</span><% else %><span class="pill err">no</span><% end %></td>
|
|
87
|
+
</tr>
|
|
88
|
+
<% end %>
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<h2>Judge agreement<span class="sub">split / unknown / order-dependent</span></h2>
|
|
94
|
+
<div class="card">
|
|
95
|
+
<div class="skill-desc">
|
|
96
|
+
<span class="pill <%= r.counts[:split].positive? ? 'warn' : 'ok' %>"><%= r.counts[:split] %> split</span>
|
|
97
|
+
<span class="pill <%= r.counts[:unknown].positive? ? 'warn' : 'ok' %>"><%= r.counts[:unknown] %> unknown</span>
|
|
98
|
+
<span class="pill <%= @agreement[:order_dependent].positive? ? 'warn' : 'ok' %>"><%= @agreement[:order_dependent] %> flipped with order</span>
|
|
99
|
+
<span class="muted">of <%= r.counts[:judged] %> judged pairs</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
|
|
104
|
+
<h2>Judge a batch</h2>
|
|
105
|
+
<div class="card">
|
|
106
|
+
<form method="post" action="/studio/parity" class="actions">
|
|
107
|
+
<%== csrf_tag %>
|
|
108
|
+
<label>
|
|
109
|
+
Agent (all stores when blank)
|
|
110
|
+
<input type="text" name="agent" placeholder="agent-id">
|
|
111
|
+
</label>
|
|
112
|
+
<label>
|
|
113
|
+
Limit
|
|
114
|
+
<input type="number" name="limit" value="50" min="1" max="200">
|
|
115
|
+
</label>
|
|
116
|
+
<button type="submit" class="primary">Judge <%= @pending %> complete pair(s)</button>
|
|
117
|
+
</form>
|
|
118
|
+
<div class="skill-desc muted">
|
|
119
|
+
Cost: <span class="mono"><%= @judge_cost %></span> provider calls
|
|
120
|
+
(<%= @pending %> pairs × <%= @criterion.rule.min_judge_models %> judges × 2 orders).
|
|
121
|
+
<% if Array((@judge_models || {})["judges"]).empty? %>
|
|
122
|
+
<span class="pill err">no judges configured in settings['evals'] — the press will refuse</span>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<h2>Recent pairs<span class="sub">inbound · incumbent · insika</span></h2>
|
|
128
|
+
<div class="card">
|
|
129
|
+
<ul class="history">
|
|
130
|
+
<% @pairs.each do |p| %>
|
|
131
|
+
<li>
|
|
132
|
+
<span class="pill <%= { open: 'warn', complete: 'info', silent: 'info', judged: 'ok', incomplete: 'err' }.fetch(p.status, 'info') %>"><%= p.status %></span>
|
|
133
|
+
<span class="mono"><%= p.id[0, 12] %></span>
|
|
134
|
+
<% if p.agent %><span class="mono"><%= p.agent %></span><% end %>
|
|
135
|
+
<% if p.outcome %><span class="pill"><%= p.outcome %></span><% end %>
|
|
136
|
+
<% if p.session_id %><a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(p.session_id) %>">session →</a><% end %>
|
|
137
|
+
<div class="skill-desc muted"><strong>customer:</strong> <%= p.inbound.to_s %></div>
|
|
138
|
+
<div class="skill-desc"><strong>incumbent:</strong> <%= p.incumbent_reply.to_s %></div>
|
|
139
|
+
<div class="skill-desc"><strong>insika:</strong> <%= p.insika_reply.to_s %></div>
|
|
140
|
+
<% if p.verdict.is_a?(Hash) && presence(p.verdict["reason"]) %>
|
|
141
|
+
<div class="skill-desc muted">why: <%= p.verdict["reason"] %></div>
|
|
142
|
+
<% end %>
|
|
143
|
+
</li>
|
|
144
|
+
<% end %>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
</select>
|
|
16
16
|
</label>
|
|
17
17
|
<label class="chat-field">session
|
|
18
|
-
<input type="text" name="session_id" value="<%= @session_id %>" placeholder="empty = new conversation"
|
|
18
|
+
<input type="text" name="session_id" value="<%= @session_id %>" placeholder="empty = new conversation"
|
|
19
|
+
list="recent-sessions" autocomplete="off">
|
|
20
|
+
<datalist id="recent-sessions">
|
|
21
|
+
<% Array(@recent).each do |s| %>
|
|
22
|
+
<option value="<%= s.id %>" label="<%= [time_ago(s.updated_at), presence(session_preview(s).to_s[0, 60])].compact.join(" · ") %>">
|
|
23
|
+
<% end %>
|
|
24
|
+
</datalist>
|
|
19
25
|
</label>
|
|
20
26
|
<span class="pill info chat-status" data-live-transcript-target="status">disconnected</span>
|
|
21
27
|
<details class="pin chat-pin">
|
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
<a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
|
|
13
13
|
</header>
|
|
14
14
|
|
|
15
|
+
<% briefing = @session.respond_to?(:briefing) ? (@session.briefing || {}) : {} %>
|
|
16
|
+
<% fields = briefing["fields"] || {} %>
|
|
17
|
+
<% next_step = briefing["next_step"] %>
|
|
18
|
+
<% if fields.any? || next_step %>
|
|
19
|
+
<div class="card">
|
|
20
|
+
<div class="trace-head">
|
|
21
|
+
<h2>Briefing</h2>
|
|
22
|
+
<span class="muted">this conversation's working state — read-only, written by the agent</span>
|
|
23
|
+
</div>
|
|
24
|
+
<% fields.sort.each do |name, value| %>
|
|
25
|
+
<div class="kv"><span><%= name %></span><div><%= value %></div></div>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% if next_step %>
|
|
28
|
+
<div class="kv"><span>next step</span><div><%= next_step %></div></div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
15
33
|
<% ctx_traces = @context_traces || [] %>
|
|
16
34
|
<% unless ctx_traces.empty? %>
|
|
17
35
|
<div class="card tooltrace">
|
|
@@ -32,6 +50,13 @@
|
|
|
32
50
|
<summary>
|
|
33
51
|
<span class="badge"><%= c["used"] %> / <%= c["cap"] %></span>
|
|
34
52
|
<strong>context</strong>
|
|
53
|
+
<%# the prefix cache-hit of the turn + the first block
|
|
54
|
+
whose bytes changed vs the previous turn (PII-free category id). %>
|
|
55
|
+
<% cache = c["cache"] %>
|
|
56
|
+
<% if cache && cache["hit_pct"] %><span class="badge"><%= cache["hit_pct"] %>% cached</span><% end %>
|
|
57
|
+
<% if cache && cache["invalidation_reason"] %>
|
|
58
|
+
<span class="lat">broke: <%= cache["invalidation_reason"] %></span>
|
|
59
|
+
<% end %>
|
|
35
60
|
<% unless skills.empty? %>
|
|
36
61
|
<span class="badge">skills <%= skills.length %></span>
|
|
37
62
|
<% end %>
|
|
@@ -41,7 +66,7 @@
|
|
|
41
66
|
</summary>
|
|
42
67
|
<% (c["categories"] || {}).sort_by { |_k, v| -(v["tokens"] || 0) }.each do |name, cat| %>
|
|
43
68
|
<div class="kv"><span><%= name %></span>
|
|
44
|
-
<div><%= cat["tokens"] %> tokens · <%= cat["fragments"] %> fragment(s)<%== " · pinned" if (cat["pinned"] || 0).positive? %>
|
|
69
|
+
<div><%= cat["tokens"] %> tokens · <%= cat["fragments"] %> fragment(s)<%== " · pinned" if (cat["pinned"] || 0).positive? %><%== " · identity" if cat["layer"] == "identity" %>
|
|
45
70
|
<%# WHICH skills the turn injected AND WHY — the only after-the-fact %>
|
|
46
71
|
<%# record of activation once the bodies arrive by context instead of a %>
|
|
47
72
|
<%# tool call. The reason is the half the operator was missing: a name %>
|
|
@@ -100,6 +125,36 @@
|
|
|
100
125
|
</div>
|
|
101
126
|
<% end %>
|
|
102
127
|
|
|
128
|
+
<%# the session's stage history — outcomes folded to the agent's
|
|
129
|
+
declared stages (unmapped kinds render the raw outcome: the hole is visible).
|
|
130
|
+
Without a declaration the block renders the raw outcomes. %>
|
|
131
|
+
<% history = @stage_history || {} %>
|
|
132
|
+
<% unless history.empty? %>
|
|
133
|
+
<div class="card">
|
|
134
|
+
<div class="trace-head">
|
|
135
|
+
<h2>Stage history</h2>
|
|
136
|
+
<span class="muted">
|
|
137
|
+
<% if history[:declaration] %>
|
|
138
|
+
<%= history[:declaration].stages.join(" → ") %> · outcome funnel
|
|
139
|
+
<% else %>
|
|
140
|
+
no funnel declared — raw outcomes
|
|
141
|
+
<% end %>
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="kv">
|
|
145
|
+
<% history[:rows].each do |row| %>
|
|
146
|
+
<% if row[:stage] %>
|
|
147
|
+
<span><%= row[:outcome] %></span>
|
|
148
|
+
<div>→ <strong><%= row[:stage] %></strong> <span class="lat"><%= row[:at] %></span></div>
|
|
149
|
+
<% else %>
|
|
150
|
+
<span><%= row[:outcome] %></span>
|
|
151
|
+
<div><span class="lat">not a declared stage</span> <span class="lat"><%= row[:at] %></span></div>
|
|
152
|
+
<% end %>
|
|
153
|
+
<% end %>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<% end %>
|
|
157
|
+
|
|
103
158
|
<div class="card transcript">
|
|
104
159
|
<% if Array(@session.messages).empty? %>
|
|
105
160
|
<div class="empty">This session has no messages.</div>
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
<label>max_retries<input type="text" name="max_retries" value="<%= @settings["max_retries"] %>" inputmode="numeric"></label>
|
|
40
40
|
<label>turn_timeout (s)<input type="text" name="turn_timeout" value="<%= @settings["turn_timeout"] %>" inputmode="numeric"></label>
|
|
41
41
|
<label>tool_timeout (s)<input type="text" name="tool_timeout" value="<%= @settings["tool_timeout"] %>" inputmode="numeric"></label>
|
|
42
|
+
<label>Memory TTL (days)<input type="text" name="memory_ttl_days" value="<%= @settings["memory_ttl_days"] %>" inputmode="numeric" placeholder="blank = off"></label>
|
|
42
43
|
</div>
|
|
44
|
+
<p class="muted">Memory TTL: how many days a customer memory cell lives before the daily sweep prunes it (per-fact <code>expires_at</code> overrides always win). Editing here sets the platform default for <strong>every</strong> cell; a per-tenant map authored in the settings record is replaced by this save.</p>
|
|
43
45
|
</form>
|
|
44
46
|
</div>
|
|
45
47
|
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
<% new = @selected == "" %>
|
|
40
40
|
<div class="drill-pane-head">
|
|
41
41
|
<div class="title">
|
|
42
|
-
<nav class="crumbs"><a href="/studio/skills">skills</a> / <span><%= new ? "new" : @selected %></span><% if scope %> / <span>for <%= scope %></span><% end %></nav>
|
|
43
42
|
<strong class="mono"><%= new ? "New skill" : @selected %><% if scope %> <span class="pill warn">specialized for <%= scope %></span><% end %></strong>
|
|
44
43
|
</div>
|
|
45
44
|
<button type="submit" form="skill-form" class="btn primary" data-turbo-submits-with="Saving…">Save skill</button>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
</header>
|
|
7
7
|
|
|
8
|
-
<p class="muted matrix-hint">GLOBAL prompts/rules: the runtime injects these files <strong>before</strong> each
|
|
8
|
+
<p class="muted matrix-hint">GLOBAL prompts/rules: the runtime injects these files <strong>before</strong> each agent's own identity, every turn. Store-backed and versioned (like per-agent prompts). Empty = no global injection (the prompt stays exactly as before).</p>
|
|
9
9
|
|
|
10
10
|
<% if @system_files.empty? %>
|
|
11
11
|
<div class="empty">No system files. Create one below (e.g. <code>HOUSE_RULES.md</code>) for a rule that applies to every agent.</div>
|
|
@@ -88,6 +88,19 @@
|
|
|
88
88
|
<% end %>
|
|
89
89
|
</section>
|
|
90
90
|
|
|
91
|
+
<% if @task.timing && !@task.timing.empty? %>
|
|
92
|
+
<section class="section">
|
|
93
|
+
<div class="section-head"><h2>Timing <span class="muted">· the turn's latency windows</span></h2></div>
|
|
94
|
+
<div class="card">
|
|
95
|
+
<ul class="kv">
|
|
96
|
+
<% @task.timing.each do |key, value| %>
|
|
97
|
+
<li><span class="k mono"><%= key %></span><span class="v mono"><%= value %></span></li>
|
|
98
|
+
<% end %>
|
|
99
|
+
</ul>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
<% end %>
|
|
103
|
+
|
|
91
104
|
<section class="section">
|
|
92
105
|
<div class="section-head"><h2>Latest checkpoint</h2></div>
|
|
93
106
|
<% if @checkpoint.nil? %>
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
<nav class="crumbs"><span>tasks</span></nav>
|
|
4
4
|
<h1>Tasks<span class="sub"><%= @tasks.size %> task(s)</span></h1>
|
|
5
5
|
</div>
|
|
6
|
-
<
|
|
6
|
+
<div class="head-actions">
|
|
7
|
+
<%== agent_filter_form("/studio/tasks", @agent) %>
|
|
8
|
+
<a class="btn" href="/studio/approvals">Approvals →</a>
|
|
9
|
+
</div>
|
|
7
10
|
</header>
|
|
8
11
|
|
|
9
12
|
<% if @tasks.empty? %>
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
<% a = @sel_agent %>
|
|
48
48
|
<div class="drill-pane-head">
|
|
49
49
|
<div class="title">
|
|
50
|
-
<nav class="crumbs"><a href="/studio/tools">tools</a> / <span><%= a.id %></span></nav>
|
|
51
50
|
<strong class="mono"><%= a.id %></strong>
|
|
52
51
|
</div>
|
|
53
52
|
<button type="submit" form="tools-form" class="btn primary" data-turbo-submits-with="Saving…">Save tools</button>
|
data/lib/insika/task_store.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Insika
|
|
|
33
33
|
}.freeze
|
|
34
34
|
|
|
35
35
|
Task = Data.define(:id, :status, :command, :session_id, :executions,
|
|
36
|
-
:mailbox_state, :created_at, :updated_at)
|
|
36
|
+
:mailbox_state, :timing, :created_at, :updated_at)
|
|
37
37
|
Execution = Data.define(:attempt, :started_at, :finished_at, :outcome, :error)
|
|
38
38
|
|
|
39
39
|
def initialize(store:)
|
|
@@ -131,6 +131,18 @@ module Insika
|
|
|
131
131
|
to_task(record)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# writes the turn's timing breakdown onto the task record, once,
|
|
135
|
+
# when the turn completes. Best-effort by the caller's contract — a failure
|
|
136
|
+
# here must never re-fail an already-committed turn. `timing` is DATA (string
|
|
137
|
+
# keys, JSON types); deep_stringify makes a symbol-keyed to_h durable.
|
|
138
|
+
def record_timing(id, timing)
|
|
139
|
+
record = fetch!(id)
|
|
140
|
+
record["timing"] = deep_stringify(timing)
|
|
141
|
+
record["updated_at"] = timestamp
|
|
142
|
+
@store.set(SCOPE, key_for(id), record)
|
|
143
|
+
to_task(record)
|
|
144
|
+
end
|
|
145
|
+
|
|
134
146
|
# (`collect`): appends a fragment to a task's message while it is
|
|
135
147
|
# still waiting at the door. -> Task.
|
|
136
148
|
#
|
|
@@ -185,6 +197,13 @@ module Insika
|
|
|
185
197
|
end
|
|
186
198
|
end
|
|
187
199
|
|
|
200
|
+
# -> true | false (did it exist?). For Retention (WS8) — a purged session's
|
|
201
|
+
# tasks go with it, terminal tasks only (a live :running task must never
|
|
202
|
+
# be deleted under its own fiber).
|
|
203
|
+
def delete(id)
|
|
204
|
+
@store.delete(SCOPE, key_for(id))
|
|
205
|
+
end
|
|
206
|
+
|
|
188
207
|
private
|
|
189
208
|
|
|
190
209
|
def key_for(id)
|
|
@@ -228,6 +247,7 @@ module Insika
|
|
|
228
247
|
session_id: record["session_id"],
|
|
229
248
|
executions: record["executions"].map { |e| to_execution(e) },
|
|
230
249
|
mailbox_state: record["mailbox_state"],
|
|
250
|
+
timing: record["timing"],
|
|
231
251
|
created_at: record["created_at"],
|
|
232
252
|
updated_at: record["updated_at"]
|
|
233
253
|
)
|