samagotchi 0.29.0 → 0.30.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -1
  3. data/docs/cli.md +4 -4
  4. data/docs/hooks.md +1 -1
  5. data/docs/plugins.md +1 -1
  6. data/docs/sessions.md +2 -2
  7. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  8. data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
  9. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
  10. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  11. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  12. data/lib/samagotchi/command_steps.rb +35 -15
  13. data/lib/samagotchi/host_registry.rb +4 -0
  14. data/lib/samagotchi/kernel_loop.rb +19 -6
  15. data/lib/samagotchi/llm/chat_loop.rb +12 -6
  16. data/lib/samagotchi/model_list_store.rb +117 -0
  17. data/lib/samagotchi/model_profile.rb +43 -0
  18. data/lib/samagotchi/send_command.rb +5 -3
  19. data/lib/samagotchi/session.rb +5 -0
  20. data/lib/samagotchi/session_manager.rb +9 -2
  21. data/lib/samagotchi/session_metrics.rb +6 -3
  22. data/lib/samagotchi/terminal_ui/formatting.rb +7 -4
  23. data/lib/samagotchi/terminal_ui.rb +35 -5
  24. data/lib/samagotchi/tool_activity.rb +6 -2
  25. data/lib/samagotchi/tools/delegate.rb +14 -10
  26. data/lib/samagotchi/tools/execute.rb +3 -1
  27. data/lib/samagotchi/version.rb +1 -1
  28. data/lib/samagotchi/web/public/activity.js +2 -1
  29. data/lib/samagotchi/web/public/app.js +9 -7
  30. data/lib/samagotchi/web/public/command_view.js +19 -5
  31. data/lib/samagotchi/web/public/index.html +14 -6
  32. data/lib/samagotchi/web/public/stage_model.js +18 -0
  33. data/lib/samagotchi/web/public/stage_view.js +16 -11
  34. data/lib/samagotchi/web/public/task_stop.js +11 -2
  35. data/lib/samagotchi/web/public/timing.js +12 -7
  36. data/lib/samagotchi/web/public/turn_events.js +3 -1
  37. data/lib/samagotchi/web/public/turn_model.js +16 -11
  38. data/lib/samagotchi/web/public/turn_view.js +26 -3
  39. data/lib/samagotchi/worker.rb +21 -13
  40. metadata +2 -1
@@ -14,10 +14,12 @@ export function turnOutput(data) {
14
14
 
15
15
  // The end line for a turn whose worker went away mid-turn (the footer's
16
16
  // stop, a crash): no turn_canceled comes, so the page ends the turn itself.
17
+ // The footer's stop is the user's, drawn as a cancel is (\u25A0, timing.js
18
+ // cancelLineText); a worker that exited on its own is a failure (\u2715).
17
19
  // null when no turn was running.
18
20
  export function workerGoneText({ turnRunning, stopped = false }) {
19
21
  if (!turnRunning) return null;
20
- return `\u2715 canceled (${stopped ? "session stopped" : "worker exited"})`;
22
+ return stopped ? "\u25A0 canceled (session stopped)" : "\u2715 canceled (worker exited)";
21
23
  }
22
24
 
23
25
  // How long a turn's answer waits for the after_turn hooks' answer_display
@@ -176,21 +176,26 @@ export function failedCount(rows) {
176
176
  // call's title ("edit lib/a.rb"), else the tools it worked with, else
177
177
  // "thinking"; then its call count and how many failed.
178
178
  export function genLabel(gen) {
179
+ const { head, calls } = genLabelParts(gen);
180
+ return calls ? `${head} · ${calls}` : head;
181
+ }
182
+
183
+ // genLabel in its parts: +head+, +calls+ ("3 tool calls (1 failed)", or ""
184
+ // without calls) and +repeats+: the head is words the open step's body
185
+ // starts with (its narration, its first call's title), so an open step's
186
+ // summary can say only its calls (turn_view.js labelHtml).
187
+ export function genLabelParts(gen) {
179
188
  const line = String(gen.text || "").trim().split("\n")[0].trim();
180
- const calls = gen.tools.length;
189
+ const calls = gen.tools.length ? toolCallsText(gen.tools) : "";
181
190
  const first = gen.tools[0];
182
- let head;
183
- if (line) {
184
- head = cut(line);
185
- } else if (first?.title) {
186
- head = cut(`${toolName(first)} ${first.title}`);
187
- } else if (calls) {
191
+ if (line) return { head: cut(line), calls, repeats: true };
192
+ if (first?.title) return { head: cut(`${toolName(first)} ${first.title}`), calls, repeats: true };
193
+ if (gen.tools.length) {
188
194
  const names = [...new Set(gen.tools.map(toolName))];
189
- head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
190
- } else {
191
- return "thinking";
195
+ const head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
196
+ return { head, calls, repeats: false };
192
197
  }
193
- return calls ? `${head} · ${toolCallsText(gen.tools)}` : head;
198
+ return { head: "thinking", calls: "", repeats: false };
194
199
  }
195
200
 
196
201
  // The block's summary line: "working · 3 steps · 8 tool calls · execute ×7 · …"
@@ -399,7 +399,7 @@ export function createTurnView({
399
399
  // an ellipsed label): at close, and again when a late tool result lands
400
400
  // in a step that already closed.
401
401
  function writeClosedLabel(entry, gen) {
402
- entry.summary.textContent = model.genLabel(gen);
402
+ setLabel(entry.summary, gen);
403
403
  entry.el.classList.toggle("has-failed", model.failedCount(gen.tools) > 0);
404
404
  }
405
405
 
@@ -551,7 +551,7 @@ export function createTurnView({
551
551
  }
552
552
  entry.text = document.createElement("div");
553
553
  entry.text.className = "gen-text";
554
- entry.summary.textContent = model.genLabel({ ...gen, text: "" });
554
+ setLabel(entry.summary, { ...gen, text: "" });
555
555
  model.takeAnswer(turn, gen);
556
556
  // Cards and hook notices in a step that goes (below, or with the whole
557
557
  // block for a plain answer) stay: before the answer, out of the block,
@@ -731,6 +731,29 @@ function defaultPlaceAnswer(appendToHistory) {
731
731
  };
732
732
  }
733
733
 
734
+ // A closed step's summary (model.genLabel). When its head is the words the
735
+ // body starts with (the narration's first line, the first call's title)
736
+ // and it has calls, the summary is marked .repeats and its parts are
737
+ // spans: an open step shows only its calls (index.html), so it doesn't say
738
+ // the same words twice. Its text stays genLabel's either way.
739
+ // { repeats, html } (the summary's inside).
740
+ function labelParts(gen) {
741
+ const { head, calls, repeats } = model.genLabelParts(gen);
742
+ if (!repeats || !calls) return { repeats: false, html: escapeHtml(model.genLabel(gen)) };
743
+ return { repeats: true, html: `<span class="gen-head">${escapeHtml(head)}</span><span class="gen-sep"> · </span><span class="gen-calls">${escapeHtml(calls)}</span>` };
744
+ }
745
+
746
+ function labelHtml(gen) {
747
+ const { repeats, html } = labelParts(gen);
748
+ return `<summary${repeats ? ' class="repeats"' : ""}>${html}</summary>`;
749
+ }
750
+
751
+ function setLabel(summary, gen) {
752
+ const { repeats, html } = labelParts(gen);
753
+ summary.classList.toggle("repeats", repeats);
754
+ summary.innerHTML = html;
755
+ }
756
+
734
757
  // A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, the full command (an
735
758
  // execute/task_create's view), output (its "[tool]" tag dropped, 300 chars, the rest on hover), plus the call's
736
759
  // duration from its record.
@@ -811,7 +834,7 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
811
834
  const steers = (group.steps[k].steers || []).map((i) => steerHtml(i, false)).join("");
812
835
  const tools = gen.tools.length ? `<div class="activity-body">${gen.tools.map((r) => reloadRowHtml(r, thumbs)).join("")}</div>` : "";
813
836
  const failed = model.failedCount(gen.tools) ? " has-failed" : "";
814
- return `<details class="gen${failed}"><summary>${escapeHtml(model.genLabel(gen))}</summary>${steers}${thinking}${text}${tools}</details>`;
837
+ return `<details class="gen${failed}">${labelHtml(gen)}${steers}${thinking}${text}${tools}</details>`;
815
838
  });
816
839
  parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
817
840
  }
@@ -268,7 +268,7 @@ module Samagotchi
268
268
 
269
269
  Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
270
270
  @session.pending_question = nil
271
- @session.save(state_dir: @state_dir)
271
+ save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
272
272
  end
273
273
 
274
274
  # spawn_session hands the first prompt over in last_prompt, but
@@ -285,7 +285,7 @@ module Samagotchi
285
285
 
286
286
  prompt = @session.last_prompt
287
287
  @session.last_prompt = ""
288
- @session.save(state_dir: @state_dir)
288
+ save_or_log(:initial_prompt) { @session.save(state_dir: @state_dir) }
289
289
  prompt
290
290
  end
291
291
 
@@ -308,14 +308,16 @@ module Samagotchi
308
308
  return false unless queue_as_command(prompt, nil)
309
309
 
310
310
  @session.status = Session::STATUS_IDLE
311
- save_session
311
+ save_or_log(:initial_command) { save_session }
312
312
  true
313
313
  end
314
314
 
315
315
  # Add the queued context notes to the conversation (between turns only,
316
316
  # on this thread), save, then delete their files: a crash before the
317
317
  # delete leaves them claimed, and Engine#add_context_note skips a note
318
- # the saved conversation already holds. Not activity: a note alone
318
+ # the saved conversation already holds. A failed save keeps the files
319
+ # too: the next pass claims them again, finds them in the conversation
320
+ # and saves again. Not activity: a note alone
319
321
  # neither starts a turn nor keeps an idle worker up.
320
322
  def absorb_notes
321
323
  files = SessionInbox.find_new_note_files(@session_dir)
@@ -326,7 +328,8 @@ module Samagotchi
326
328
  note = SessionInbox.read_note(file)
327
329
  @engine.add_context_note(@session, note) if note
328
330
  end
329
- @session.save(state_dir: @state_dir)
331
+ return unless save_or_log(:notes) { @session.save(state_dir: @state_dir) }
332
+
330
333
  claimed.each { |file| FileUtils.rm_f(file) }
331
334
  end
332
335
 
@@ -426,7 +429,7 @@ module Samagotchi
426
429
  announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed))
427
430
  shown.each { |event| @engine.announce(event) }
428
431
  end
429
- save_session unless Array(result.changed).empty?
432
+ save_or_log(:command) { save_session } unless Array(result.changed).empty?
430
433
  user_input(command[:client_id]) if resolved
431
434
  @continue_offer.after_command(result, resolved: resolved)
432
435
  @continue_offer.run_continue_turn(command) if result.resume
@@ -515,16 +518,21 @@ module Samagotchi
515
518
  unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
516
519
  SessionInbox.write_output(@session_dir, response)
517
520
  end
518
- save_after_turn
521
+ save_or_log(:turn) { save_session }
519
522
  end
520
523
 
521
- # The turn's own save: the turn ended fine and its messages are in
522
- # memory, so a save that fails here (disk full, permissions; the next
523
- # save writes them) must not take the worker down with it.
524
- def save_after_turn
525
- save_session
524
+ # A save between turns (the turn's own, a command's, the notes', the
525
+ # first prompt's): what it saves is in memory, so one that fails (disk
526
+ # full, permissions; the next save writes it) is logged and must not
527
+ # take the worker down with it.
528
+ # @param at [Symbol] which save, for the log
529
+ # @return [Boolean] whether it saved
530
+ def save_or_log(at)
531
+ yield
532
+ true
526
533
  rescue SystemCallError, IOError => e
527
- Log.exception(:worker, "save_failed", e)
534
+ Log.exception(:worker, "save_failed", e, at: at)
535
+ false
528
536
  end
529
537
 
530
538
  # Back to the conversation before the failed turn, as the REPL does (so
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.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -301,6 +301,7 @@ files:
301
301
  - lib/samagotchi/memory_bundle/uninstaller.rb
302
302
  - lib/samagotchi/memory_paths.rb
303
303
  - lib/samagotchi/model_catalog.rb
304
+ - lib/samagotchi/model_list_store.rb
304
305
  - lib/samagotchi/model_overlay.rb
305
306
  - lib/samagotchi/model_profile.rb
306
307
  - lib/samagotchi/model_ref.rb