samagotchi 0.71.0 → 0.72.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/docs/architecture.md +14 -8
  4. data/docs/cli.md +3 -2
  5. data/docs/configuration.md +23 -13
  6. data/docs/hooks.md +1 -1
  7. data/docs/internals/anthropic.md +1 -1
  8. data/docs/internals/engine.md +38 -13
  9. data/docs/internals/llm-context-live.md +1 -1
  10. data/docs/internals/prompt-caching.md +6 -0
  11. data/docs/plugins.md +18 -7
  12. data/docs/sessions.md +3 -1
  13. data/docs/testing.md +10 -1
  14. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  15. data/lib/samagotchi/bridge.rb +5 -2
  16. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  17. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  18. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  19. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  20. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  21. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  22. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  23. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  24. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  25. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  26. data/lib/samagotchi/config.rb +7 -25
  27. data/lib/samagotchi/context_note.rb +2 -6
  28. data/lib/samagotchi/effective_model.rb +1 -1
  29. data/lib/samagotchi/engine.rb +91 -213
  30. data/lib/samagotchi/engine_peers.rb +2 -0
  31. data/lib/samagotchi/event_routes.rb +83 -0
  32. data/lib/samagotchi/hook_routes.rb +16 -37
  33. data/lib/samagotchi/host_config.rb +1 -1
  34. data/lib/samagotchi/host_keys.rb +0 -2
  35. data/lib/samagotchi/paths.rb +13 -0
  36. data/lib/samagotchi/plugin/api.rb +11 -5
  37. data/lib/samagotchi/plugin/peer.rb +5 -8
  38. data/lib/samagotchi/prompt_files.rb +2 -4
  39. data/lib/samagotchi/session.rb +2 -1
  40. data/lib/samagotchi/session_manager.rb +6 -16
  41. data/lib/samagotchi/session_model_settings.rb +227 -0
  42. data/lib/samagotchi/session_setup.rb +2 -1
  43. data/lib/samagotchi/system_prompt.rb +18 -13
  44. data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
  45. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  46. data/lib/samagotchi/tool_activity.rb +1 -0
  47. data/lib/samagotchi/tool_declarations.rb +4 -0
  48. data/lib/samagotchi/toolkit.rb +55 -24
  49. data/lib/samagotchi/toolkit_binding.rb +7 -5
  50. data/lib/samagotchi/toolkit_command.rb +28 -5
  51. data/lib/samagotchi/tools/builtins.rb +2 -1
  52. data/lib/samagotchi/tools/delegate.rb +32 -6
  53. data/lib/samagotchi/tools/list_sessions.rb +2 -2
  54. data/lib/samagotchi/tools/peers.rb +5 -2
  55. data/lib/samagotchi/tools/register_reminder.rb +1 -0
  56. data/lib/samagotchi/tools/registry.rb +1 -1
  57. data/lib/samagotchi/tools/tool_index.rb +15 -4
  58. data/lib/samagotchi/turn_note.rb +4 -1
  59. data/lib/samagotchi/version.rb +1 -1
  60. data/lib/samagotchi/web/app.rb +13 -10
  61. data/lib/samagotchi/web/public/app.js +20 -18
  62. data/lib/samagotchi/web/public/bubbles.js +1 -1
  63. data/lib/samagotchi/web/public/cards_place.js +6 -10
  64. data/lib/samagotchi/web/public/chains.js +1 -1
  65. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  66. data/lib/samagotchi/web/public/composer.js +13 -6
  67. data/lib/samagotchi/web/public/context_chips.js +25 -12
  68. data/lib/samagotchi/web/public/data.js +5 -1
  69. data/lib/samagotchi/web/public/format.js +5 -0
  70. data/lib/samagotchi/web/public/sessions_list.js +2 -3
  71. data/lib/samagotchi/web/public/stage_frame.js +1 -1
  72. data/lib/samagotchi/web/public/stage_view.js +8 -2
  73. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  74. data/lib/samagotchi/web/public/timing.js +1 -1
  75. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  76. data/lib/samagotchi/web/session_hub.rb +8 -0
  77. metadata +3 -1
@@ -32,22 +32,20 @@ export function turnAnswerEl(turnEls) {
32
32
  }
33
33
 
34
34
  // @param historyEl the history
35
- // @param appendToHistory (el) puts +el+ at the history's end (or the stage's)
36
- // @param isNearBottom () => whether the history follows its end
37
35
  // @param turnDom the running turn's view (its card(el) takes a turn's card)
38
36
  // @param findInTurnRoots (selector) => the matching elements in the history
39
37
  // and the stage
40
38
  // @param selected () => the open session's id
41
39
  // @param clientId this tab's id in the session's events
42
40
  // @param questionCards the history's question cards (question_cards.js)
43
- // @param bubbles {removeHintIfEmpty, showGuardrailWarning, showHookNotice}
41
+ // @param bubbles {removeHintIfEmpty, placeFollowing, showGuardrailWarning, showHookNotice}
44
42
  // (bubbles.js createBubbles)
45
43
  // @param followWokenWorker () follows the worker a card's command woke
46
44
  // @param api {getSession, sendCommand} (data.js)
47
45
  // @return {cardElement(id), upsertCard(card, {before}), placeSnapshotCards(entries),
48
46
  // refreshCardBodies(), onCardAction(event)}
49
47
  export function createCardsPlace({
50
- historyEl, appendToHistory, isNearBottom, turnDom, findInTurnRoots, selected, clientId, questionCards, bubbles,
48
+ historyEl, turnDom, findInTurnRoots, selected, clientId, questionCards, bubbles,
51
49
  followWokenWorker, api = { getSession, sendCommand },
52
50
  }) {
53
51
  function cardElement(id) {
@@ -73,10 +71,7 @@ export function createCardsPlace({
73
71
  const el = drawCard(document.createElement(isNoticeCard(card) ? "details" : "div"), card);
74
72
  const asks = (card.actions || []).some((a) => a && a.command);
75
73
  if (card.in_turn && !before && !asks && turnDom.card(el)) return;
76
- const follow = isNearBottom();
77
- if (before) historyEl.insertBefore(el, before);
78
- else appendToHistory(el);
79
- if (follow) historyEl.scrollTop = historyEl.scrollHeight;
74
+ bubbles.placeFollowing(el, before);
80
75
  }
81
76
 
82
77
  // A snapshot's cards and notices, each above the first turn after it; a
@@ -170,8 +165,9 @@ export function createCardsPlace({
170
165
  button.disabled = true;
171
166
  api.sendCommand(id, button.dataset.command, { clientId, card: true })
172
167
  .then(() => {
173
- // The command woke the worker: follow its stream if none is open.
174
- followWokenWorker();
168
+ // The command woke the worker: follow its stream if none is open
169
+ // (and the page still shows that session).
170
+ if (selected() === id) followWokenWorker();
175
171
  })
176
172
  .catch((error) => {
177
173
  err.textContent = error.message;
@@ -206,7 +206,7 @@ export function chainRowsTemplate(f, { activeId = null, now = Date.now() } = {})
206
206
  export function continueRefusal(error, sessions = []) {
207
207
  const body = error?.body || {};
208
208
  if (error?.code === "continued" && body.next_id) return { kind: "open", id: body.next_id };
209
- const text = String(error?.message || "refused").replace(/\s*\(\d{3}\)$/, "");
209
+ const text = String(error?.message || "refused");
210
210
  if (error?.code === "open_children" && Array.isArray(body.ids) && body.ids.length) {
211
211
  const byId = new Map(sessions.map((s) => [s.id, s]));
212
212
  const children = body.ids.map((id) => {
@@ -84,10 +84,12 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
84
84
  show(data, false);
85
85
  }
86
86
 
87
+ // The click goes on to the document: another chip's popover (or the
88
+ // context popover) open meanwhile closes there. This one's own closer
89
+ // leaves its chip's click alone.
87
90
  el.addEventListener("click", (e) => {
88
91
  const anchor = e.target.closest(anchorSel);
89
92
  if (!anchor) return;
90
- e.stopPropagation();
91
93
  if (isOpen()) close();
92
94
  else open(anchor);
93
95
  });
@@ -60,20 +60,26 @@ export function routeSessionLine(prompt, { commands = [], images = 0, delivery =
60
60
  // followWokenWorker() the command woke the worker: follow its stream
61
61
  // refreshHistory()
62
62
  // failed(line, text) a failed command's bubble
63
+ // failedElsewhere(line, text) a command to a session no longer open failed
63
64
  // ownedByTui() a plain terminal holds the session
64
65
  // composer: {busy(), clear(), done()} the composer's send button and text
65
66
  export function createComposerCommands(deps) {
66
- const { session, clientId, sendCommand, followWokenWorker, refreshHistory, failed, ownedByTui, composer } = deps;
67
+ const { session, clientId, sendCommand, followWokenWorker, refreshHistory, failed, failedElsewhere, ownedByTui,
68
+ composer } = deps;
67
69
 
68
- // A line typed in the composer (routeComposerLine's {command}).
70
+ // The page may open another session while a command is in flight: what
71
+ // follows is about the session it went to (+id+), so the open one's
72
+ // worker, owner and history are left alone then.
69
73
  async function send(line) {
74
+ const id = session();
70
75
  composer.busy();
71
76
  try {
72
- await sendCommand(session(), line, { clientId });
77
+ await sendCommand(id, line, { clientId });
73
78
  composer.clear();
74
79
  refreshHistory();
75
- followWokenWorker();
80
+ if (session() === id) followWokenWorker();
76
81
  } catch (e) {
82
+ if (session() !== id) return failedElsewhere(line, e.message);
77
83
  if (e.code === "owned_by_tui") {
78
84
  ownedByTui();
79
85
  return;
@@ -93,10 +99,11 @@ export function createComposerCommands(deps) {
93
99
  if (!id) return false;
94
100
  try {
95
101
  await sendCommand(id, line, { clientId });
96
- followWokenWorker();
102
+ if (session() === id) followWokenWorker();
97
103
  return true;
98
104
  } catch (e) {
99
- failed(line, e.message);
105
+ if (session() === id) failed(line, e.message);
106
+ else failedElsewhere(line, e.message);
100
107
  return false;
101
108
  }
102
109
  }
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { html, nothing, render } from "./vendor/lit-html.js";
11
11
  import { keepFocus } from "./keep_focus.js";
12
+ import { popoverPlace } from "./chip_popover.js";
13
+ import { cantText } from "./format.js";
12
14
 
13
15
  // "now", "3m", "2h", "4d" since an ISO time; "" for none.
14
16
  export function ageText(iso, now = Date.now()) {
@@ -177,13 +179,24 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
177
179
  render(popoverTemplate(row, { text }), pop);
178
180
  }
179
181
 
180
- // Above the chip, inside the window with a 16px gutter.
182
+ // At the chip as the session bar's chips place theirs (popoverPlace):
183
+ // above it, or below when the room above is short; capped to the room
184
+ // (it scrolls), inside the window with a 16px gutter.
181
185
  function place(chip) {
182
- const rect = chip.getBoundingClientRect();
183
186
  const width = Math.min(380, win.innerWidth - 32);
184
187
  pop.style.width = `${width}px`;
185
- pop.style.left = `${Math.max(16, Math.min(rect.left, win.innerWidth - width - 16))}px`;
186
- pop.style.bottom = `${Math.max(16, win.innerHeight - rect.top + 8)}px`;
188
+ pop.style.maxHeight = "";
189
+ const at = popoverPlace(chip.getBoundingClientRect(), { width, height: pop.offsetHeight,
190
+ innerWidth: win.innerWidth, innerHeight: win.innerHeight });
191
+ pop.style.left = `${at.left}px`;
192
+ pop.style.top = at.top === undefined ? "" : `${at.top}px`;
193
+ pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
194
+ if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
195
+ }
196
+
197
+ function placeAtOpenChip() {
198
+ const chip = openName && el.querySelector(".ctx-chip.open");
199
+ if (chip) place(chip);
187
200
  }
188
201
 
189
202
  function open(name, chip) {
@@ -231,7 +244,7 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
231
244
  } catch (e) {
232
245
  if (id !== sessionId || openName !== ADD) return;
233
246
  // The same form again: what the user typed stays; the URL gets focus.
234
- drawAdd({ busy: false, error: String(e.message).replace(/ \(\d+\)$/, "") });
247
+ drawAdd({ busy: false, error: e.message });
235
248
  pop.querySelector('input[name="url"]')?.focus();
236
249
  }
237
250
  }
@@ -255,8 +268,10 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
255
268
  if (!row) return;
256
269
  text = data.text || "";
257
270
  fill(row);
271
+ // The text makes it taller: placed again.
272
+ placeAtOpenChip();
258
273
  } catch (e) {
259
- toast(`Can't read ${name}: ${String(e.message).replace(/ \(\d+\)$/, "")}`);
274
+ toast(cantText(`read ${name}`, e));
260
275
  }
261
276
  }
262
277
  async function detach() {
@@ -271,14 +286,15 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
271
286
  close();
272
287
  await refresh();
273
288
  } catch (e) {
274
- toast(`Can't detach ${row.name}: ${String(e.message).replace(/ \(\d+\)$/, "")}`);
289
+ toast(cantText(`detach ${row.name}`, e));
275
290
  }
276
291
  }
277
292
 
278
293
  el.addEventListener("click", (e) => {
279
294
  const chip = e.target.closest(".ctx-chip");
280
295
  if (!chip) return;
281
- e.stopPropagation();
296
+ // On to the document: a session-bar chip's popover closes (the close
297
+ // below leaves clicks in +el+ alone).
282
298
  if (chip.dataset.act === "add") {
283
299
  if (openName === ADD) close();
284
300
  else openAdd(chip);
@@ -301,10 +317,7 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
301
317
  doc.addEventListener("keydown", (e) => {
302
318
  if (e.key === "Escape" && openName) close();
303
319
  });
304
- win.addEventListener("resize", () => {
305
- const chip = openName && el.querySelector(".ctx-chip.open");
306
- if (chip) place(chip);
307
- });
320
+ win.addEventListener("resize", placeAtOpenChip);
308
321
  win.addEventListener("focus", () => refresh());
309
322
  // The ages move on by themselves.
310
323
  win.setInterval(() => { if (rows.length) drawChips(); }, 60_000);
@@ -6,7 +6,11 @@ export function api(path, opts = {}) {
6
6
  }).then(async (r) => {
7
7
  const j = await r.json().catch(() => ({}));
8
8
  if (!r.ok) {
9
- const error = new Error(`${j.detail || j.error || r.statusText} (${r.status})`);
9
+ // The server's words as they are: what the page shows the user. The
10
+ // status goes in only when it sent none (a proxy's bare 502, a crash's
11
+ // 500): then it is all there is to go on.
12
+ const words = j.detail || j.error;
13
+ const error = new Error(words || `${r.statusText || "HTTP error"} (${r.status})`);
10
14
  error.status = r.status;
11
15
  // The server's error code ({error: "owned_by_tui", detail}): what
12
16
  // the page matches on, never the message's words.
@@ -168,6 +168,11 @@ export function goneSessionNotice(id, why) {
168
168
  return `Session ${String(id).slice(0, 8)} was ${why === "deleted" ? "deleted" : "not found"}.`;
169
169
  }
170
170
 
171
+ // A failed action's toast: what failed and why (api()'s error says why).
172
+ export function cantText(what, error) {
173
+ return `Can't ${what}: ${String(error?.message ?? error)}`;
174
+ }
175
+
171
176
  // A session in the list after its live status changed (a turn started or
172
177
  // ended): that is activity now, so its card's "N min ago" says so too, and
173
178
  // the change came from a worker's stream, so the card says "live".
@@ -264,10 +264,9 @@ export function batchTargets(picked, shownIds, archive, sessionsById) {
264
264
  });
265
265
  }
266
266
 
267
- // A refusal's reason as the card shows it: the server's words, without
268
- // the api() status suffix.
267
+ // A refusal's reason as the card shows it: the server's words.
269
268
  function refusalText(error) {
270
- return String(error?.message || "refused").replace(/\s*\(\d{3}\)$/, "");
269
+ return String(error?.message || "refused");
271
270
  }
272
271
 
273
272
  // Runs +act+ over +ids+ one at a time (a live session's stop waits on the
@@ -13,7 +13,7 @@ import { easeScrollTo } from "./scroll.js";
13
13
  const GROW_MS = 180;
14
14
  const GROW_EASE = "cubic-bezier(.2,.7,.3,1)";
15
15
  // The room between the stage and the composer card (index.html's margin-bottom).
16
- const STAGE_GAP = 10;
16
+ export const STAGE_GAP = 10;
17
17
 
18
18
  export function reducedMotion() {
19
19
  return matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -127,6 +127,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
127
127
  let endedAt = 0;
128
128
  let thinkingLine = null; // { step, text } from the live step's ticker
129
129
  let flash = null; // { kind, text, until } a notice or nudge in the trail
130
+ let flashTimer = null; // its end's redraw
130
131
  let collapsed = readCollapsed();
131
132
  let cloudOpen = false;
132
133
  // A ref's "show in turn" opened a collapsed stage (not kept).
@@ -252,7 +253,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
252
253
  if (!what) return;
253
254
  flash = { ...what, until: Date.now() + FLASH_MS };
254
255
  scheduleRefresh();
255
- setTimeout(scheduleRefresh, FLASH_MS + 50);
256
+ clearTimeout(flashTimer);
257
+ flashTimer = setTimeout(scheduleRefresh, FLASH_MS + 50);
256
258
  }
257
259
 
258
260
  // The answer reads from its top (no tail pin in the stage): scrolled to
@@ -408,6 +410,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
408
410
  promptSlot.classList.remove("full");
409
411
  thinkingLine = null;
410
412
  flash = null;
413
+ clearTimeout(flashTimer);
411
414
  setCloud(false);
412
415
  stage.classList.remove("plain", "ended", "waiting", "held-wait", "asking", "in-use");
413
416
  stage.style.removeProperty("--live-held");
@@ -590,8 +593,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
590
593
  // composer's delivery line).
591
594
  current: () => view.current(),
592
595
  phase: () => liveSlots(view.current().turn, { pending: pendingCard() || asksCard() }).phase,
596
+ // Another session opens, or this one renders again: a turn still
597
+ // sliding away is dropped (its history is about to be drawn anew),
598
+ // never moved into whatever history shows now.
593
599
  reset() {
594
- if (folding) finishFold();
600
+ folding = null;
595
601
  release();
596
602
  },
597
603
  // The stage's extras while it holds a turn (a card, a prompt for its
@@ -150,12 +150,15 @@ export function createStreamHandlers(deps) {
150
150
  // An after_turn hook presented the answer (AnswerDisplay) or attached
151
151
  // refs to it (AnswerRefs: refs, a count; display null when only refs):
152
152
  // it comes after turn_completed, so re-read the answer once that read
153
- // is done.
153
+ // is done: the session's open then, so a switch meanwhile leaves the
154
+ // other session's answers alone.
154
155
  answer_display: async (data) => {
156
+ const id = selected();
155
157
  try {
156
158
  await liveTurn.endRead;
157
- if (!selected() || (!data.display && !data.refs)) return;
158
- const d = await getSession(selected(), { tail: true, recent: true });
159
+ if (!id || id !== selected() || (!data.display && !data.refs)) return;
160
+ const d = await getSession(id, { tail: true, recent: true });
161
+ if (id !== selected()) return;
159
162
  if (Array.isArray(d.messages)) applyFinalMarkdown(d.messages);
160
163
  } catch (_) {
161
164
  } finally {
@@ -278,7 +278,7 @@ function noMatch(record) {
278
278
  // every assistant message is one step. Its calls take the turn's records by
279
279
  // tool, in call order (each the next record of its tool, consumed with the
280
280
  // ones it skipped), not by iteration: an empty retry spends an iteration
281
- // that left no message (as the TUI's join, attached_loop.rb join_duration).
281
+ // that left no message (as the TUI's join, attached_loop.rb join_record).
282
282
  // The last message is the answer when it has text and made no call (and the
283
283
  // turn is no canceled multi-step one); its thinking stays behind as a step,
284
284
  // as it does live.
@@ -5,7 +5,8 @@
5
5
  // worker's snapshot or a command_ran; the server's own reading of the
6
6
  // session file has no lists). Left out is reachable (find_tools and
7
7
  // call_tool reach them) and removed (only /toolkit add brings one back).
8
- // Changing it is /toolkit (no popover yet).
8
+ // Changing it is /toolkit (no popover yet). A kit with a prompt text has
9
+ // it on the summary too (prompt), and the tooltip ends with it.
9
10
 
10
11
  import { html, nothing } from "./vendor/lit-html.js";
11
12
 
@@ -24,6 +25,7 @@ export function chipModel(summary) {
24
25
  parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
25
26
  }
26
27
  parts.push("/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.");
28
+ if (summary.prompt) parts.push(`Prompt: ${summary.prompt}`);
27
29
  return { text: `tools ${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" ") };
28
30
  }
29
31
 
@@ -172,6 +172,14 @@ module Samagotchi
172
172
  end
173
173
  end
174
174
 
175
+ # A session's folder as the projection holds it (its file's, read at
176
+ # the last scan or touch): what a live read that loads no session
177
+ # file checks refs against. nil for a session the hub hasn't seen.
178
+ # @return [String, nil]
179
+ def working_directory(id)
180
+ @monitor.synchronize { @sessions[id]&.session&.working_directory }
181
+ end
182
+
175
183
  # Register a sink (#call(event)). With +snapshot:+ the current
176
184
  # projection comes back with the handle, taken in the same step, so
177
185
  # every event after it is newer than it.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.71.0
4
+ version: 0.72.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -279,6 +279,7 @@ files:
279
279
  - lib/samagotchi/empty_answer_retry.rb
280
280
  - lib/samagotchi/engine.rb
281
281
  - lib/samagotchi/engine_peers.rb
282
+ - lib/samagotchi/event_routes.rb
282
283
  - lib/samagotchi/events.rb
283
284
  - lib/samagotchi/extension_load.rb
284
285
  - lib/samagotchi/file_ref.rb
@@ -459,6 +460,7 @@ files:
459
460
  - lib/samagotchi/session_inbox.rb
460
461
  - lib/samagotchi/session_manager.rb
461
462
  - lib/samagotchi/session_metrics.rb
463
+ - lib/samagotchi/session_model_settings.rb
462
464
  - lib/samagotchi/session_observer.rb
463
465
  - lib/samagotchi/session_retention.rb
464
466
  - lib/samagotchi/session_setting.rb