samagotchi 0.15.0 → 0.17.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +2 -0
  4. data/docs/architecture.md +23 -0
  5. data/docs/cli.md +7 -4
  6. data/docs/configuration.md +7 -5
  7. data/docs/guardrails.md +16 -2
  8. data/docs/internals/background-tasks.md +1 -0
  9. data/docs/memory.md +39 -0
  10. data/docs/plugins.md +2 -0
  11. data/docs/sessions.md +10 -4
  12. data/docs/sub-agent.md +16 -3
  13. data/docs/testing.md +47 -0
  14. data/lib/samagotchi/answer_display.rb +6 -2
  15. data/lib/samagotchi/bridge/card_store.rb +16 -1
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +26 -5
  17. data/lib/samagotchi/bridge.rb +122 -7
  18. data/lib/samagotchi/bridge_client.rb +33 -6
  19. data/lib/samagotchi/bundle_command.rb +13 -2
  20. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  21. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  22. data/lib/samagotchi/bundles/system/self_map.md +3 -1
  23. data/lib/samagotchi/empty_answer_retry.rb +2 -1
  24. data/lib/samagotchi/engine.rb +82 -18
  25. data/lib/samagotchi/kernel_loop.rb +16 -5
  26. data/lib/samagotchi/llm/chat_loop.rb +14 -8
  27. data/lib/samagotchi/llm/model_result.rb +12 -7
  28. data/lib/samagotchi/log_subscriber.rb +1 -0
  29. data/lib/samagotchi/memory_bundle/builder.rb +22 -0
  30. data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
  31. data/lib/samagotchi/memory_bundle/installer.rb +60 -21
  32. data/lib/samagotchi/memory_bundle/profile.rb +9 -4
  33. data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
  34. data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
  35. data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
  36. data/lib/samagotchi/memory_bundle/trash.rb +57 -0
  37. data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
  38. data/lib/samagotchi/parent_report.rb +8 -0
  39. data/lib/samagotchi/plugin/context.rb +14 -2
  40. data/lib/samagotchi/question_desk.rb +97 -18
  41. data/lib/samagotchi/relay.rb +88 -0
  42. data/lib/samagotchi/relay_desk.rb +73 -0
  43. data/lib/samagotchi/relay_verifier.rb +113 -0
  44. data/lib/samagotchi/relay_watcher.rb +46 -0
  45. data/lib/samagotchi/reply_wait.rb +10 -1
  46. data/lib/samagotchi/self_report.rb +49 -3
  47. data/lib/samagotchi/send_command.rb +45 -6
  48. data/lib/samagotchi/session.rb +15 -2
  49. data/lib/samagotchi/session_manager.rb +3 -2
  50. data/lib/samagotchi/session_metrics.rb +154 -48
  51. data/lib/samagotchi/sessions_command.rb +10 -4
  52. data/lib/samagotchi/system_prompt.rb +45 -4
  53. data/lib/samagotchi/terminal_ui/attach_launcher.rb +11 -2
  54. data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -21
  55. data/lib/samagotchi/terminal_ui/event_renderer.rb +8 -2
  56. data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
  57. data/lib/samagotchi/terminal_ui/question_prompt.rb +48 -2
  58. data/lib/samagotchi/terminal_ui/question_slot.rb +5 -1
  59. data/lib/samagotchi/terminal_ui.rb +24 -0
  60. data/lib/samagotchi/tool_declarations.rb +2 -2
  61. data/lib/samagotchi/tools/builtins.rb +11 -3
  62. data/lib/samagotchi/tools/delegate_relay.rb +220 -0
  63. data/lib/samagotchi/tools/delegate_wait.rb +72 -6
  64. data/lib/samagotchi/tools/peers.rb +7 -4
  65. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  66. data/lib/samagotchi/turn_note.rb +30 -2
  67. data/lib/samagotchi/version.rb +1 -1
  68. data/lib/samagotchi/web/app.rb +22 -1
  69. data/lib/samagotchi/web/public/app.js +60 -33
  70. data/lib/samagotchi/web/public/index.html +7 -0
  71. data/lib/samagotchi/web/public/notify.js +22 -5
  72. data/lib/samagotchi/web/public/question_card.js +45 -3
  73. data/lib/samagotchi/web/public/question_cards.js +49 -1
  74. data/lib/samagotchi/web/public/sessions_list.js +8 -1
  75. data/lib/samagotchi/web/public/stage_model.js +8 -3
  76. data/lib/samagotchi/web/public/stage_view.js +20 -3
  77. data/lib/samagotchi/web/public/strip.js +11 -0
  78. data/lib/samagotchi/web/public/timing.js +37 -15
  79. data/lib/samagotchi/web/public/turn_events.js +22 -3
  80. data/lib/samagotchi/web/public/turn_view.js +34 -12
  81. data/lib/samagotchi/worker.rb +28 -2
  82. data/lib/samagotchi/worker_sidecar.rb +4 -1
  83. metadata +8 -1
@@ -88,7 +88,9 @@ export function timedTurnIndexes(items) {
88
88
  // the last, in order; a text-less iteration leaves no message, so a text
89
89
  // can sit one step early: inspection, not truth) and its answer (the last
90
90
  // assistant message). Assistant messages before any prompt are answers on
91
- // their own. The turn index counts prompts, as timedTurnIndexes does. The
91
+ // their own. A turn that ended with no answer (the server's empty_answer
92
+ // item) has no answer: its texts are steps, and `emptyAnswer` is that
93
+ // item's index. The turn index counts prompts, as timedTurnIndexes does. The
92
94
  // user's lines merged into the running turn (`merged`, the server's
93
95
  // Steer::INPUT_KIND) stay in it: the turn's `merged` list.
94
96
  export function turnGroups(items, timing) {
@@ -110,13 +112,18 @@ export function turnGroups(items, timing) {
110
112
  } else if (item?.role === "assistant") {
111
113
  if (turn) turn.texts.push(i);
112
114
  else groups.push({ kind: "answer", answer: i });
115
+ } else if (item?.role === "empty_answer" && turn) {
116
+ turn.emptyAnswer = i;
113
117
  }
114
118
  });
115
119
  return groups.map((g) => {
116
120
  if (g.kind !== "turn") return g;
117
121
  const group = withSteers(turnGroup(g, items, timing), g.steers, items);
122
+ // A turn that ended with no answer: its notice (the item's index) in
123
+ // the answer's place.
124
+ const ended = g.emptyAnswer == null ? group : { ...group, emptyAnswer: g.emptyAnswer };
118
125
  // A turn with no merged lines keeps its shape.
119
- return g.merged.length ? { ...group, merged: g.merged } : group;
126
+ return g.merged.length ? { ...ended, merged: g.merged } : ended;
120
127
  });
121
128
  }
122
129
 
@@ -150,7 +157,8 @@ function promptRecord(timing, g) {
150
157
  function turnGroup(g, items, timing) {
151
158
  const record = promptRecord(timing, g);
152
159
  const tools = record ? (timing?.toolRecords || []).filter((r) => r.turn_id === record.id) : [];
153
- const stepsOnly = canceledSteps(g, record, tools);
160
+ // A turn with no answer has none of its texts as one: they are steps.
161
+ const stepsOnly = canceledSteps(g, record, tools) || g.emptyAnswer != null;
154
162
  if (g.texts.some((i) => items[i].parts)) return partsTurn(g, items, record, tools, stepsOnly);
155
163
  const maxIteration = tools.reduce((m, r) => Math.max(m, Number(r.iteration) || 0), 0);
156
164
  const stepTexts = stepsOnly ? g.texts : g.texts.slice(0, -1);
@@ -177,37 +185,51 @@ function recordRows(tools, iteration) {
177
185
  }
178
186
 
179
187
  // A turn whose saved messages carry their parts (the server's ?parts=1):
180
- // every assistant message is one generation, so the k-th is iteration k
181
- // and its calls pair with that iteration's records by position. The last
182
- // message is the answer when it has text and made no call (and the turn
183
- // is no canceled multi-step one); its thinking stays behind as a step, as
184
- // it does live.
188
+ // every assistant message is one step. Its calls take the turn's records by
189
+ // tool, in call order (each the next record of its tool, consumed with the
190
+ // ones it skipped), not by iteration: an empty retry spends an iteration
191
+ // that left no message (as the TUI's join, attached_loop.rb join_duration).
192
+ // The last message is the answer when it has text and made no call (and the
193
+ // turn is no canceled multi-step one); its thinking stays behind as a step,
194
+ // as it does live.
185
195
  function partsTurn(g, items, record, tools, stepsOnly = false) {
186
196
  const last = g.texts[g.texts.length - 1];
187
197
  const lastItem = items[last];
188
198
  const answer = !stepsOnly && String(lastItem.content ?? "") !== "" && !lastItem.parts?.tools?.length ? last : null;
199
+ const pool = [...tools].sort((a, b) =>
200
+ (Number(a.iteration) || 0) - (Number(b.iteration) || 0) || (Number(a.call_index) || 0) - (Number(b.call_index) || 0));
189
201
  const steps = g.texts.map((i, k) => {
190
202
  const iteration = k + 1;
191
203
  const parts = items[i].parts || {};
192
- const records = recordRows(tools, iteration);
193
204
  const rows = Array.isArray(parts.tools)
194
205
  ? parts.tools.map((call, j) => {
195
- const r = records.find((row) => row.key === `${iteration}:${j + 1}`);
206
+ const r = takeRecord(pool, call.tool);
196
207
  return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...call };
197
208
  })
198
- : records;
209
+ : [];
199
210
  const thinking = String(parts.thinking || "");
200
211
  if (i === answer) return thinking ? { i: null, iteration, thinking, tools: [] } : null;
201
212
  return { i, iteration, thinking, tools: rows };
202
213
  }).filter(Boolean);
203
- // Records past the saved messages (a generation that saved nothing).
204
- const maxIteration = tools.reduce((m, r) => Math.max(m, Number(r.iteration) || 0), 0);
205
- for (let iteration = g.texts.length + 1; iteration <= maxIteration; iteration += 1) {
206
- steps.push({ i: null, iteration, thinking: "", tools: recordRows(tools, iteration) });
214
+ // Records no saved message took (a generation that saved nothing): a
215
+ // step per iteration of theirs, after the messages'.
216
+ let iteration = g.texts.length;
217
+ for (const n of [...new Set(pool.map((r) => Number(r.iteration) || 0))]) {
218
+ iteration += 1;
219
+ const rows = recordRows(pool, n).map((row) => ({ ...row, key: `${iteration}:${row.key.split(":")[1]}` }));
220
+ steps.push({ i: null, iteration, thinking: "", tools: rows });
207
221
  }
208
222
  return { kind: "turn", turnIndex: g.turnIndex, user: g.user, answer, record, steps };
209
223
  }
210
224
 
225
+ // The next record of +tool+ in +pool+ (call order), removed with the ones
226
+ // before it; undefined when there is none (the pool stays as it was).
227
+ function takeRecord(pool, tool) {
228
+ const at = pool.findIndex((r) => String(r.tool) === String(tool));
229
+ if (at < 0) return undefined;
230
+ return pool.splice(0, at + 1)[at];
231
+ }
232
+
211
233
  // Append +el+ to the history, above +timingEl+ while it is a running turn's
212
234
  // live timing line, so that line stays under the turn's activity, thinking
213
235
  // and answer.
@@ -64,6 +64,15 @@ export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
64
64
  return `↻ ${what}, asking again (${attempt}/${of})`;
65
65
  }
66
66
 
67
+ // The one muted line for a turn that ended with no answer (turn_completed's
68
+ // turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
69
+ // it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
70
+ export function emptyAnswerLine({ retries = 0 } = {}) {
71
+ const count = Number(retries) || 0;
72
+ const after = count > 0 ? ` (after ${count} ${count === 1 ? "retry" : "retries"})` : "";
73
+ return `no answer: the model returned nothing${after}`;
74
+ }
75
+
67
76
  // The text of a turn's row from a snapshot entry (CardStore): a hook's
68
77
  // notice as "<label>: text", the loop's asking-again row as its line.
69
78
  export function noticeLine(entry = {}) {
@@ -88,9 +97,19 @@ export function initWaitLine({ tasks } = {}) {
88
97
  return `waiting for ${labels.length ? labels.join(" · ") : "plugins"}…`;
89
98
  }
90
99
 
91
- // A composer line for the worker's command route rather than a prompt.
92
- export function isCommandLine(text) {
93
- return /^[/!]/.test(String(text || "").trim());
100
+ // Whether a composer line in an open session is one of its commands
101
+ // (+commands+: the session GET's listing), as the terminals decide
102
+ // (Commands::Registry#lookup): a "!" line with something after the "!", or
103
+ // a listed name alone or followed by a space; a terminal's own (/stats,
104
+ // /exit) too, which the page answers itself (webLocalReply). An unknown
105
+ // "/word" (a path, a typo) goes to the model as a prompt, as in a terminal.
106
+ export function sessionCommandLine(text, commands) {
107
+ const line = String(text || "").trim();
108
+ if (/^!\s*\S/.test(line)) return true;
109
+ if (!line.startsWith("/")) return false;
110
+ if (webLocalReply(line)) return true;
111
+ return (Array.isArray(commands) ? commands : [])
112
+ .some((c) => c && typeof c.name === "string" && (line === c.name || line.startsWith(`${c.name} `)));
94
113
  }
95
114
 
96
115
  // The terminal's own commands (a button in the web, or none yet): the
@@ -16,7 +16,7 @@ import { activityDiffHtml } from "./diff_view.js";
16
16
  import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
17
17
  import { thumbsHtml } from "./images.js";
18
18
  import { shouldFollowScroll } from "./scroll.js";
19
- import { hookNoticeLabel } from "./turn_events.js";
19
+ import { emptyAnswerLine, hookNoticeLabel } from "./turn_events.js";
20
20
  import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
21
21
  import { createSentenceFeed } from "./sentences.js";
22
22
  import { createTicker } from "./thinking_ticker.js";
@@ -465,11 +465,15 @@ export function createTurnView({
465
465
  // +heldHeight+ (the live box's height, measured before the step closed) the
466
466
  // bubble is held at that height, scrolled to its tail, until answerReady()
467
467
  // (the rendered markdown landed) or 1.5 s reveals it.
468
- function promote(gen, { heldHeight = 0 } = {}) {
468
+ // +endNote+: the notice of a turn that ended with no answer (app.js
469
+ // builds it), placed where the answer bubble would go.
470
+ function promote(gen, { heldHeight = 0, endNote = null } = {}) {
469
471
  const entry = genEls.get(gen);
470
472
  const bubble = entry.text;
471
- bubble.className = "bubble output";
472
- if (!gen.text) bubble.textContent = "";
473
+ // No text (an empty answer): no bubble, not even an empty one left in
474
+ // the step.
475
+ if (gen.text) bubble.className = "bubble output";
476
+ else bubble.remove();
473
477
  // A hidden tab gets no pop: nobody sees it, and its timers are throttled.
474
478
  const holding = useHold && heldHeight > 0 && !!gen.text && !document.hidden;
475
479
  let holdHeight = 0;
@@ -512,11 +516,12 @@ export function createTurnView({
512
516
  }
513
517
  pieces.push(...cards);
514
518
  if (gen.text) pieces.push(bubble);
519
+ else if (endNote) pieces.push(endNote);
515
520
  placeAnswer(pieces, { plain: true, blockEl });
516
521
  blockEl = null;
517
522
  blockSummaryEl = null;
518
523
  } else {
519
- placeAnswer(gen.text ? [...cards, bubble] : cards, { plain: false, blockEl });
524
+ placeAnswer(gen.text ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
520
525
  // The last step, left with its thinking only, reads "thinking" once.
521
526
  if (!gen.tools.length && gen.thinking) unwrapThinking(entry);
522
527
  }
@@ -524,8 +529,10 @@ export function createTurnView({
524
529
  }
525
530
 
526
531
  // Returns the cards that left the block (card.js leavesBlock), detached:
527
- // app.js puts them after the turn's end line.
528
- function turnEnded({ kind }) {
532
+ // app.js puts them after the turn's end line. +endNote+ (a completed turn
533
+ // with no answer): its notice, in the answer's place; left unplaced
534
+ // with no turn to end (app.js then shows it itself).
535
+ function turnEnded({ kind, endNote = null }) {
529
536
  // Once per turn: a second end (a re-dispatched event) would promote again.
530
537
  if (!turn || turn.ended) return [];
531
538
  const last = model.currentGen(turn);
@@ -541,7 +548,8 @@ export function createTurnView({
541
548
  const heldHeight = box ? box.getBoundingClientRect().height : 0;
542
549
  if (pendingSteers.length) flushSteers();
543
550
  apply(model.turnEnded(turn));
544
- if (promoting) promote(last, { heldHeight });
551
+ if (promoting) promote(last, { heldHeight, endNote });
552
+ else if (endNote) placeAnswer([endNote], { plain: false, blockEl });
545
553
  const kept = [];
546
554
  if (blockEl) {
547
555
  updateBlockSummary();
@@ -678,6 +686,13 @@ function reloadRowHtml(r, thumbs) {
678
686
  `${r.images?.length ? thumbs(r.images) : ""}${r.diff ? activityDiffHtml(r.diff) : ""}</div>`;
679
687
  }
680
688
 
689
+ // The notice of a turn that ended with no answer: a muted row like the
690
+ // loop's retry row, where the answer would be (live: promote()).
691
+ export const EMPTY_ANSWER_CLASS = "bubble hook-notice empty-answer";
692
+ function emptyAnswerHtml(item) {
693
+ return `<div class="${EMPTY_ANSWER_CLASS}">${escapeHtml(emptyAnswerLine(item))}</div>`;
694
+ }
695
+
681
696
  // A reloaded history in the turn view (renderHistory's object path): per
682
697
  // turn the prompt, a collapsed block of its steps when it had any (texts,
683
698
  // and with the server's parts the thinking, params and output, from the
@@ -708,9 +723,13 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
708
723
  const prompt = items[group.user];
709
724
  const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}><div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
710
725
  const [only] = group.steps;
711
- const noBlock = !group.steps.length ||
712
- (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
713
- if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
726
+ const ended = group.answer !== null || group.emptyAnswer != null;
727
+ const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
728
+ // A lone thinking step before an answer (or the notice of a turn with
729
+ // none): no block, as live.
730
+ const plainThinking = group.steps.length === 1 && ended && textless && !only.tools.length && only.thinking;
731
+ const noBlock = !group.steps.length || plainThinking;
732
+ if (plainThinking) {
714
733
  // A plain answer with thinking: the collapsed thinking block, as live.
715
734
  parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
716
735
  } else if (group.steps.length) {
@@ -736,9 +755,12 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
736
755
  // after its steps and before its answer.
737
756
  parts.push((group.merged || []).map((i) =>
738
757
  `<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}><div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>`).join(""));
758
+ // A turn that ended with no answer: chi's muted notice where the answer
759
+ // would be, before the timing, as live.
760
+ if (group.emptyAnswer != null) parts.push(`${emptyAnswerHtml(items[group.emptyAnswer])}${timingHtml(group)}`);
739
761
  // A turn with no answer (canceled, or running: no record yet) shows its
740
762
  // timing after its prompt and steps, where the live view leaves it.
741
- if (group.answer === null) parts.push(timingHtml(group));
763
+ else if (group.answer === null) parts.push(timingHtml(group));
742
764
  else parts.push(answerHtml(items[group.answer], timingHtml(group)));
743
765
  // A canceled turn ends with its cancel line, as the live view left it.
744
766
  parts.push(cancelLineHtml(group.record, escapeHtml));
@@ -173,7 +173,7 @@ module Samagotchi
173
173
  absorb_notes
174
174
 
175
175
  if (prompt = take_initial_prompt)
176
- run_prompt(prompt, nil)
176
+ run_prompt(prompt, nil) unless initial_command(prompt)
177
177
  next
178
178
  end
179
179
 
@@ -269,6 +269,29 @@ module Samagotchi
269
269
  prompt
270
270
  end
271
271
 
272
+ # A session command sent as a message (chi send -m "/model x", a web
273
+ # page's first message) runs as the command, as the Bridge does for a
274
+ # POST /turn; an unknown /word stays a prompt. Queued: the loop's next
275
+ # pass runs it.
276
+ # @return [Boolean] whether +text+ was one
277
+ def queue_as_command(text, origin)
278
+ return false unless @engine.command_registry.command?(text.to_s)
279
+
280
+ @bridge.queue_command(text, client_id: origin&.dig(:client_id))
281
+ true
282
+ end
283
+
284
+ # The first prompt as a command: the session was saved as running for a
285
+ # turn that won't run.
286
+ # @return [Boolean] whether it was one
287
+ def initial_command(prompt)
288
+ return false unless queue_as_command(prompt, nil)
289
+
290
+ @session.status = Session::STATUS_IDLE
291
+ save_session
292
+ true
293
+ end
294
+
272
295
  # Add the queued context notes to the conversation (between turns only,
273
296
  # on this thread), save, then delete their files: a crash before the
274
297
  # delete leaves them claimed, and Engine#add_context_note skips a note
@@ -293,7 +316,10 @@ module Samagotchi
293
316
 
294
317
  begin
295
318
  message, origin, no_interrupt, images = SessionInbox.read_input(claimed_file)
296
- run_prompt(message, origin, no_interrupt: !!no_interrupt, images: images || []) unless message.to_s.strip.empty?
319
+ return if message.to_s.strip.empty?
320
+ return if Array(images).empty? && queue_as_command(message, origin)
321
+
322
+ run_prompt(message, origin, no_interrupt: !!no_interrupt, images: images || [])
297
323
  ensure
298
324
  FileUtils.rm_f(claimed_file)
299
325
  end
@@ -42,7 +42,10 @@ module Samagotchi
42
42
  return nil unless sidecar&.port&.positive?
43
43
 
44
44
  begin
45
- Socket.tcp(host, sidecar.port, connect_timeout: timeout).close
45
+ # Not Socket.tcp: with connect_timeout (Ruby 3.4, macOS 26) it hands
46
+ # back the socket of a refused connect (SO_ERROR set), so a dead
47
+ # worker's port read as live. TCPSocket raises ECONNREFUSED.
48
+ TCPSocket.new(host, sidecar.port, connect_timeout: timeout).close
46
49
  sidecar
47
50
  rescue StandardError
48
51
  FileUtils.rm_f(path(session_dir)) if unlink
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.15.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -137,6 +137,7 @@ files:
137
137
  - docs/releasing.md
138
138
  - docs/sessions.md
139
139
  - docs/sub-agent.md
140
+ - docs/testing.md
140
141
  - lib/samagotchi.rb
141
142
  - lib/samagotchi/answer_command.rb
142
143
  - lib/samagotchi/answer_display.rb
@@ -280,6 +281,7 @@ files:
280
281
  - lib/samagotchi/memory_bundle/source.rb
281
282
  - lib/samagotchi/memory_bundle/status.rb
282
283
  - lib/samagotchi/memory_bundle/system_bundle.rb
284
+ - lib/samagotchi/memory_bundle/trash.rb
283
285
  - lib/samagotchi/memory_bundle/uninstaller.rb
284
286
  - lib/samagotchi/memory_paths.rb
285
287
  - lib/samagotchi/model_catalog.rb
@@ -310,6 +312,10 @@ files:
310
312
  - lib/samagotchi/question_desk.rb
311
313
  - lib/samagotchi/recap_setup.rb
312
314
  - lib/samagotchi/recap_store.rb
315
+ - lib/samagotchi/relay.rb
316
+ - lib/samagotchi/relay_desk.rb
317
+ - lib/samagotchi/relay_verifier.rb
318
+ - lib/samagotchi/relay_watcher.rb
313
319
  - lib/samagotchi/reminder_queue.rb
314
320
  - lib/samagotchi/reminder_store.rb
315
321
  - lib/samagotchi/reply_wait.rb
@@ -366,6 +372,7 @@ files:
366
372
  - lib/samagotchi/tools/builtins.rb
367
373
  - lib/samagotchi/tools/cancel_reminder.rb
368
374
  - lib/samagotchi/tools/delegate.rb
375
+ - lib/samagotchi/tools/delegate_relay.rb
369
376
  - lib/samagotchi/tools/delegate_result.rb
370
377
  - lib/samagotchi/tools/delegate_wait.rb
371
378
  - lib/samagotchi/tools/edit.rb