samagotchi 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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -1
  3. data/README.md +40 -4
  4. data/bin/chi +86 -19
  5. data/docs/cli.md +108 -5
  6. data/docs/configuration.md +229 -45
  7. data/docs/desktop.md +6 -0
  8. data/docs/hooks.md +126 -5
  9. data/docs/plugins.md +68 -2
  10. data/docs/releasing.md +18 -8
  11. data/docs/sessions.md +30 -4
  12. data/lib/samagotchi/answer_display.rb +95 -0
  13. data/lib/samagotchi/archive_store.rb +90 -0
  14. data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
  15. data/lib/samagotchi/bootstrap/probe.rb +262 -0
  16. data/lib/samagotchi/bootstrap_command.rb +347 -0
  17. data/lib/samagotchi/bridge/pending_card.rb +89 -0
  18. data/lib/samagotchi/bridge/turn_accumulator.rb +14 -3
  19. data/lib/samagotchi/bridge.rb +9 -0
  20. data/lib/samagotchi/bridge_client.rb +6 -2
  21. data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
  22. data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
  23. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +358 -0
  24. data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
  25. data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
  26. data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
  27. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  28. data/lib/samagotchi/bundles/system/self_map.md +8 -2
  29. data/lib/samagotchi/client.rb +72 -13
  30. data/lib/samagotchi/config.rb +196 -36
  31. data/lib/samagotchi/desktop/macos/ChiRunner.swift +13 -7
  32. data/lib/samagotchi/desktop/macos/Panel.swift +71 -19
  33. data/lib/samagotchi/empty_answer_retry.rb +43 -0
  34. data/lib/samagotchi/engine.rb +233 -36
  35. data/lib/samagotchi/guardrails/approval.rb +9 -0
  36. data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
  37. data/lib/samagotchi/guardrails.rb +1 -0
  38. data/lib/samagotchi/hooks/registry.rb +24 -5
  39. data/lib/samagotchi/host_registry.rb +4 -3
  40. data/lib/samagotchi/idle_recap.rb +5 -1
  41. data/lib/samagotchi/kernel_loop.rb +47 -15
  42. data/lib/samagotchi/llm/chat_loop.rb +59 -20
  43. data/lib/samagotchi/llm/errors.rb +21 -3
  44. data/lib/samagotchi/llm/http.rb +42 -13
  45. data/lib/samagotchi/llm/openai_chat.rb +12 -4
  46. data/lib/samagotchi/log_subscriber.rb +18 -3
  47. data/lib/samagotchi/model_profile.rb +1 -1
  48. data/lib/samagotchi/plugin/context.rb +22 -1
  49. data/lib/samagotchi/plugin/sessions.rb +3 -1
  50. data/lib/samagotchi/reply_wait.rb +126 -0
  51. data/lib/samagotchi/sampling_settings.rb +58 -0
  52. data/lib/samagotchi/self_report.rb +1 -0
  53. data/lib/samagotchi/send_command.rb +153 -7
  54. data/lib/samagotchi/session.rb +52 -11
  55. data/lib/samagotchi/session_archive_command.rb +107 -0
  56. data/lib/samagotchi/session_commands.rb +11 -2
  57. data/lib/samagotchi/session_manager.rb +114 -9
  58. data/lib/samagotchi/session_metrics.rb +222 -106
  59. data/lib/samagotchi/steer.rb +72 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +39 -6
  61. data/lib/samagotchi/terminal_ui/event_renderer.rb +13 -8
  62. data/lib/samagotchi/terminal_ui/formatting.rb +31 -8
  63. data/lib/samagotchi/terminal_ui/input_support.rb +3 -0
  64. data/lib/samagotchi/terminal_ui.rb +77 -4
  65. data/lib/samagotchi/tool_activity.rb +3 -1
  66. data/lib/samagotchi/tools/builtins.rb +15 -4
  67. data/lib/samagotchi/tools/delegate_wait.rb +26 -69
  68. data/lib/samagotchi/tools/execute.rb +52 -14
  69. data/lib/samagotchi/tools/task_runtime.rb +19 -0
  70. data/lib/samagotchi/tools/task_wait.rb +27 -3
  71. data/lib/samagotchi/turn_note.rb +60 -6
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/vision_support.rb +2 -6
  74. data/lib/samagotchi/web/app.rb +88 -4
  75. data/lib/samagotchi/web/public/activity.js +10 -1
  76. data/lib/samagotchi/web/public/annotate_presets.js +26 -0
  77. data/lib/samagotchi/web/public/annotations.js +13 -0
  78. data/lib/samagotchi/web/public/app.js +437 -88
  79. data/lib/samagotchi/web/public/card.js +5 -3
  80. data/lib/samagotchi/web/public/chat_view.js +10 -1
  81. data/lib/samagotchi/web/public/copy.js +20 -4
  82. data/lib/samagotchi/web/public/ctx.js +15 -0
  83. data/lib/samagotchi/web/public/data.js +21 -6
  84. data/lib/samagotchi/web/public/format.js +9 -0
  85. data/lib/samagotchi/web/public/index.html +38 -2
  86. data/lib/samagotchi/web/public/notify.js +175 -0
  87. data/lib/samagotchi/web/public/question_card.js +2 -1
  88. data/lib/samagotchi/web/public/sessions_list.js +7 -0
  89. data/lib/samagotchi/web/public/timing.js +39 -14
  90. data/lib/samagotchi/web/public/turn_events.js +46 -0
  91. data/lib/samagotchi/web/public/turn_view.js +47 -7
  92. data/lib/samagotchi/web/server.rb +8 -4
  93. data/lib/samagotchi/web/session_hub.rb +2 -1
  94. data/lib/samagotchi/web/session_summary.rb +24 -1
  95. data/lib/samagotchi/worker.rb +11 -0
  96. metadata +20 -1
@@ -51,11 +51,29 @@ export function hookNoticeLabel(hook) {
51
51
  return m ? m[1] : "hook";
52
52
  }
53
53
 
54
+ // The row a step gets when the loop asks again after an empty answer.
55
+ export function emptyRetryLine({ attempt, of } = {}) {
56
+ return `↻ empty answer, asking again (${attempt}/${of})`;
57
+ }
58
+
54
59
  // A composer line for the worker's command route rather than a prompt.
55
60
  export function isCommandLine(text) {
56
61
  return /^[/!]/.test(String(text || "").trim());
57
62
  }
58
63
 
64
+ // The terminal's own commands that have a button in the web: the reply
65
+ // the page shows itself (they never go to the worker, which doesn't know
66
+ // them), or null.
67
+ const WEB_LOCAL_REPLIES = {
68
+ "/archive": "/archive: use the archive button in the session bar",
69
+ "/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
70
+ };
71
+
72
+ export function webLocalReply(text) {
73
+ const name = String(text || "").trim().split(/\s+/)[0].toLowerCase();
74
+ return WEB_LOCAL_REPLIES[name] || null;
75
+ }
76
+
59
77
  // The continue card's answers, as the terminal's continue prompt reads them.
60
78
  export function continueLine(choice, reason = "") {
61
79
  const why = String(reason || "").trim();
@@ -80,6 +98,8 @@ export function commandView(event, myId) {
80
98
  modelName: event.model_name || null,
81
99
  anytime: event.anytime === true,
82
100
  commandId: event.command_id || null,
101
+ // A card's action (Nudge…): no bubble unless it says something back.
102
+ hidden: event.card === true && !event.output && event.status !== "error",
83
103
  };
84
104
  }
85
105
 
@@ -100,6 +120,27 @@ export function restoreAction(event, { myId, sentIds, composerEmpty }) {
100
120
  }, sent && composerEmpty ? event.images : null);
101
121
  }
102
122
 
123
+ // A turn that fails fast can have its prompt_restored come over the stream
124
+ // before the /turn ack names its enqueued_id: `early` (a Map) keeps such a
125
+ // restore of this tab's own prompt by that id, and the ack takes it back
126
+ // (restoreOnAck), so the prompt goes into the composer the ack would clear.
127
+ export function keepEarlyRestore(event, early, { myId, sentIds }) {
128
+ const origin = event?.origin || {};
129
+ const id = origin.enqueued_id;
130
+ if (!isOwn(origin.client_id, myId) || !id || sentIds.has(id)) return false;
131
+ early.set(id, event);
132
+ return true;
133
+ }
134
+
135
+ // The ack's enqueued_id (already in sentIds) was restored early: what the
136
+ // composer gets back (as restoreAction for an empty composer), else null.
137
+ export function restoreOnAck(enqueuedId, early, { myId, sentIds }) {
138
+ const event = enqueuedId && early.get(enqueuedId);
139
+ if (!event) return null;
140
+ early.delete(enqueuedId);
141
+ return restoreAction(event, { myId, sentIds, composerEmpty: true });
142
+ }
143
+
103
144
  // What an event does to the prompt bubbles, as a list of ops for app.js:
104
145
  // add — a bubble for a prompt this tab hasn't shown (state queued /
105
146
  // started / steered, labelled by its sender)
@@ -191,6 +232,11 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
191
232
  closeText();
192
233
  events.push({ type: "hook_notice", hook: part.hook, text: part.text, level: part.level });
193
234
  break;
235
+ // A plugin's steer: a steer-only merge, as live.
236
+ case "steer":
237
+ closeText();
238
+ events.push({ type: "pending_input_merged", count: 0, content: null, steers: [{ source: part.source, text: part.text }] });
239
+ break;
194
240
  default:
195
241
  break;
196
242
  }
@@ -13,7 +13,7 @@ import { toolName } from "./activity.js";
13
13
  import { leavesBlock } from "./card.js";
14
14
  import { activityStatusClass, activityStatusLabel, fillActivityRow } from "./chat_view.js";
15
15
  import { copySourceAttr } from "./copy.js";
16
- import { escapeHtml, messageBodyHtml, noteHtml, normalize } from "./format.js";
16
+ import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
17
17
  import { shouldFollowScroll } from "./scroll.js";
18
18
  import { hookNoticeLabel } from "./turn_events.js";
19
19
  import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
@@ -29,6 +29,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
29
29
  let blockToggled = false;
30
30
  // gen → { el, summary, thinking, text, activity, rows: Map(key → el), toggled }
31
31
  const genEls = new Map();
32
+ // Steer rows waiting for the step that answers them (steer()).
33
+ let pendingSteers = [];
32
34
  const dirty = new Set();
33
35
  let rafPending = false;
34
36
  // The promoted answer bubble while it is held at the box's height (the
@@ -69,6 +71,9 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
69
71
  const summary = document.createElement("summary");
70
72
  summary.textContent = `step ${turn.gens.length}`;
71
73
  el.appendChild(summary);
74
+ // A plugin's steers merged since the last step: this step answers them.
75
+ for (const row of pendingSteers) el.appendChild(row);
76
+ pendingSteers = [];
72
77
  const text = document.createElement("div");
73
78
  text.className = "gen-text streaming";
74
79
  // A step after the first shows "…" until its text comes (the first one
@@ -317,7 +322,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
317
322
 
318
323
  // A hook's line (hook_notice) as a row of the current step, above the row
319
324
  // of the call it judged; false with no running turn (app.js shows the
320
- // bubble then).
325
+ // bubble then). data.line is a whole row of chi's own (an empty-answer
326
+ // retry), shown as is.
321
327
  function notice(data) {
322
328
  const r = turn && model.notice(turn, data);
323
329
  if (!r) return false;
@@ -325,13 +331,34 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
325
331
  if (!r.gen.text) entry.text.textContent = "";
326
332
  const el = document.createElement("div");
327
333
  el.className = "hook-notice" + (data.level === "warn" ? " warn" : "");
328
- el.textContent = `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
334
+ el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
329
335
  const follow = isNearBottom();
330
336
  activityEl(r.gen).appendChild(el);
331
337
  if (follow) historyEl.scrollTop = historyEl.scrollHeight;
332
338
  return true;
333
339
  }
334
340
 
341
+ // A plugin's steer (pending_input_merged's steers) as the first row of
342
+ // the step that answers it: it joined the turn after the current step's
343
+ // tools, and the next generation (a new, open step) reads it; the
344
+ // current step collapses when that one starts. Held until then (a turn
345
+ // that ends first puts it in its last step). False with no running turn.
346
+ function steer(data) {
347
+ if (!turn || turn.ended) return false;
348
+ const holder = document.createElement("div");
349
+ holder.innerHTML = steerRowHtml(data);
350
+ pendingSteers.push(holder.firstChild);
351
+ return true;
352
+ }
353
+
354
+ // Held steer rows into the last step (the turn ends without another).
355
+ function flushSteers() {
356
+ const gen = turn.gens[turn.gens.length - 1];
357
+ const entry = gen && genEls.get(gen);
358
+ if (entry) for (const row of pendingSteers) entry.el.insertBefore(row, entry.thinkingWrap || entry.text);
359
+ pendingSteers = [];
360
+ }
361
+
335
362
  // A card shown during the turn (a plugin's, app.js draws it) as a row of
336
363
  // the current step, like a notice; false with no running turn (app.js
337
364
  // puts it between turns then).
@@ -376,8 +403,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
376
403
  // between turns.
377
404
  const leaving = !gen.thinking && gen.tools.length === 0;
378
405
  const plain = turn.gens.length === 1 && gen.tools.length === 0;
379
- const cards = leaving || plain ? [...entry.el.querySelectorAll(".plugin-card, .hook-notice")] : [];
380
- cards.forEach((el) => { if (el.classList.contains("hook-notice")) el.classList.add("bubble"); });
406
+ const cards = leaving || plain ? [...entry.el.querySelectorAll(".plugin-card, .hook-notice, .steer-row")] : [];
407
+ cards.forEach((el) => { if (!el.classList.contains("plugin-card")) el.classList.add("bubble"); });
381
408
  if (leaving) entry.el.remove();
382
409
  if (plain) {
383
410
  // A plain answer: no block, the bubble where it was (as the chat
@@ -425,6 +452,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
425
452
  // one shows at once.
426
453
  const box = promoting && kind === "completed" ? genEls.get(last)?.box : null;
427
454
  const heldHeight = box ? box.getBoundingClientRect().height : 0;
455
+ if (pendingSteers.length) flushSteers();
428
456
  apply(model.turnEnded(turn));
429
457
  if (promoting) promote(last, { heldHeight });
430
458
  const kept = [];
@@ -453,6 +481,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
453
481
  if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
454
482
  }
455
483
  genEls.clear();
484
+ pendingSteers = [];
456
485
  dirty.clear();
457
486
  rafPending = false;
458
487
  }
@@ -499,6 +528,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
499
528
  },
500
529
  turnEnded,
501
530
  notice,
531
+ steer,
502
532
  card,
503
533
  flush,
504
534
  // The rendered answer landed (or the re-read failed): let the held
@@ -554,28 +584,38 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
554
584
  const { body, renderedMarkdown } = messageBodyHtml(m);
555
585
  return `<div class="bubble output${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(m, renderedMarkdown)}>${body}</div>${timing}`;
556
586
  };
587
+ const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
557
588
  return turnGroups(items, timing).map((group) => {
558
589
  if (group.kind === "note") return `<div class="bubble note">${noteHtml(items[group.i])}</div>`;
590
+ if (group.kind === "steer") return steerHtml(group.i, true);
559
591
  if (group.kind === "answer") return answerHtml(items[group.answer], "");
560
592
  const prompt = items[group.user];
561
593
  const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}><div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
562
594
  const [only] = group.steps;
595
+ const noBlock = !group.steps.length ||
596
+ (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
563
597
  if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
564
598
  // A plain answer with thinking: the chat view's collapsed block, as live.
565
599
  parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
566
600
  } else if (group.steps.length) {
567
601
  const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
568
- const rows = gens.map((gen) => {
602
+ const rows = gens.map((gen, k) => {
569
603
  // A thinking-only step: the body directly under the step's own
570
604
  // "thinking" summary (no wrap that would repeat it), as live.
571
605
  const body = gen.thinking ? `<div class="thinking-body">${escapeHtml(gen.thinking)}</div>` : "";
572
606
  const thinking = body && (gen.text || gen.tools.length) ? `<details class="thinking"><summary>thinking</summary>${body}</details>` : body;
573
607
  const text = gen.text ? `<div class="gen-text">${escapeHtml(gen.text)}</div>` : "";
608
+ // The steers the step answered first, where the live rows went.
609
+ const steers = (group.steps[k].steers || []).map((i) => steerHtml(i, false)).join("");
574
610
  const tools = gen.tools.length ? `<div class="activity-body">${gen.tools.map((r) => reloadRowHtml(r, thumbs)).join("")}</div>` : "";
575
- return `<details class="gen"><summary>${escapeHtml(model.genLabel(gen))}</summary>${thinking}${text}${tools}</details>`;
611
+ return `<details class="gen"><summary>${escapeHtml(model.genLabel(gen))}</summary>${steers}${thinking}${text}${tools}</details>`;
576
612
  });
577
613
  parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
578
614
  }
615
+ // Steers with no step to sit in, or in a turn that shows no block: on
616
+ // their own after the prompt.
617
+ const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
618
+ parts.push(loose.map((i) => steerHtml(i, true)).join(""));
579
619
  // A turn with no answer (canceled, or running: no record yet) shows its
580
620
  // timing after its prompt and steps, where the live view leaves it.
581
621
  if (group.answer === null) parts.push(timingHtml(group));
@@ -9,6 +9,7 @@ require "webrick"
9
9
 
10
10
  require_relative "app"
11
11
  require_relative "session_hub"
12
+ require_relative "../config"
12
13
  require_relative "../project_scope"
13
14
  require_relative "../log"
14
15
  require_relative "../version"
@@ -51,7 +52,8 @@ module Samagotchi
51
52
  # open) its page for +dir+ and leave it be; else start one.
52
53
  # @param scope ["project", "all"] "all": the plain page, every session
53
54
  # @return [Integer] the exit status
54
- def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false, turn_view: true)
55
+ def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false, turn_view: true,
56
+ annotate_presets: Config::BY_KEY["web.annotate_presets"].default)
55
57
  host = resolve_host(host)
56
58
  port = resolve_port(port)
57
59
  url = scope_url(host, port, dir: dir, scope: scope)
@@ -65,7 +67,8 @@ module Samagotchi
65
67
  warn in_use_message(port)
66
68
  1
67
69
  else
68
- start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, turn_view: turn_view) ? 0 : 1
70
+ start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, turn_view: turn_view,
71
+ annotate_presets: annotate_presets) ? 0 : 1
69
72
  end
70
73
  rescue Interrupt
71
74
  # Ctrl-C: the server has stopped (start's ensure); one line, and the
@@ -78,13 +81,14 @@ module Samagotchi
78
81
  # from; built over the app's state dir unless given
79
82
  # @return [Boolean] false when the port was taken (said so on stderr)
80
83
  def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
81
- turn_view: true, hub: nil)
84
+ turn_view: true, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil)
82
85
  port = resolve_port(port)
83
86
  host = resolve_host(host)
84
87
  url ||= "http://#{url_host(host)}:#{port}/"
85
88
 
86
89
  hub ||= SessionHub.new(state_dir: state_dir || Session.default_state_dir, manager: manager || SessionManager)
87
- app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, turn_view: turn_view, hub: hub)
90
+ app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, turn_view: turn_view,
91
+ annotate_presets: annotate_presets, hub: hub)
88
92
  Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
89
93
  hub.start
90
94
  # Said once the port is bound: a second chi web racing for it gets
@@ -244,7 +244,8 @@ module Samagotchi
244
244
  if stamp != entry.stamp
245
245
  session = Session.summary_from_file(path)
246
246
  # Corrupt, or missing a field: not a session, as Session.list has it.
247
- return drop(id) if session.nil?
247
+ # A `chi scratch` session is never shown.
248
+ return drop(id) if session.nil? || session.scratch
248
249
 
249
250
  # Looked up once per parse (a git call for a session saved before
250
251
  # the field), never per tick.
@@ -2,7 +2,10 @@
2
2
 
3
3
  require_relative "../session"
4
4
  require_relative "../recap_store"
5
+ require_relative "../archive_store"
6
+ require_relative "../session_metrics"
5
7
  require_relative "../bridge_client"
8
+ require_relative "../bridge/pending_card"
6
9
 
7
10
  module Samagotchi
8
11
  module Web
@@ -25,6 +28,7 @@ module Samagotchi
25
28
  # @return [Hash] symbol keys
26
29
  def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil)
27
30
  used = session.respond_to?(:used_memory_names) ? Array(session.used_memory_names) : []
31
+ up = bridge_up?(session_dir, owner)
28
32
  {
29
33
  id: session.id,
30
34
  status: status,
@@ -46,13 +50,32 @@ module Samagotchi
46
50
  owner: owner&.fetch("kind", nil),
47
51
  # The saved recap's first sentence, for the session card.
48
52
  recap: RecapStore.preview(session_dir),
53
+ # How full the context was after the last turn (%), or nil.
54
+ ctx_pct: SessionMetrics.saved_context_pct(session_dir)&.round(1),
55
+ # Hidden from the strip and the list unless "include archived".
56
+ archived: ArchiveStore.archived?(session_dir),
49
57
  project_root: session.project_root(cache: root_cache),
50
58
  # A worker is reachable: its Bridge sidecar is there and the
51
59
  # owner lock is held. A sidecar a dead worker left is not up.
52
- bridge_up: bridge_up?(session_dir, owner)
60
+ bridge_up: up,
61
+ # For the tab's notifications (notify.js): an open question, the
62
+ # running turn's open card with actions, and how the last turn
63
+ # ended.
64
+ pending_question: pending_question_for(session),
65
+ pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
66
+ last_turn: session.respond_to?(:last_turn) ? session.last_turn : nil
53
67
  }
54
68
  end
55
69
 
70
+ # The open question as {id:, kind:} ("approval" for a guardrail's,
71
+ # else "question"); nil when there is none. The text stays out.
72
+ def pending_question_for(session)
73
+ pending = session.respond_to?(:pending_question) ? session.pending_question : nil
74
+ return nil unless pending.is_a?(Hash) && pending[:id]
75
+
76
+ { id: pending[:id], kind: pending[:kind].to_s == "approval" ? "approval" : "question" }
77
+ end
78
+
56
79
  # status is turn state (idle/running). The live worker's snapshot is the
57
80
  # truth; on disk, a "running" with no live owner was left by a worker
58
81
  # that died mid-turn.
@@ -7,6 +7,7 @@ require_relative "turn_note"
7
7
  require_relative "context_note"
8
8
  require_relative "worker_idle_exit"
9
9
  require_relative "session_manager"
10
+ require_relative "archive_store"
10
11
  require_relative "log"
11
12
  require_relative "turn_flow"
12
13
  require_relative "session_commands"
@@ -286,6 +287,7 @@ module Samagotchi
286
287
  # list); the Engine resets it to idle when it ends.
287
288
  @session.status = Session::STATUS_RUNNING
288
289
  @session.save(state_dir: @state_dir)
290
+ user_input(origin&.dig(:client_id))
289
291
  drop_continue_offer(origin)
290
292
  @turn_flow.before_prompt_turn
291
293
  @merged_this_turn = []
@@ -381,6 +383,7 @@ module Samagotchi
381
383
  shown.each { |event| @engine.announce(event) }
382
384
  end
383
385
  @session.save(state_dir: @state_dir) unless Array(result.changed).empty? || stopped_on_disk?
386
+ user_input(command[:client_id]) if resolved
384
387
  # An offer answered without a turn ("no") is activity: the recap
385
388
  # written at the offer (it says the turn stopped) gets rewritten.
386
389
  @engine.record_activity if resolved && !result.resume
@@ -432,6 +435,7 @@ module Samagotchi
432
435
  status: status, output: text[0, COMMAND_OUTPUT_LIMIT], changed: changed.map(&:to_s),
433
436
  model_name: @engine.effective_model_name }
434
437
  event[:anytime] = true if anytime
438
+ event[:card] = true if command[:card]
435
439
  event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
436
440
  @engine.announce(event)
437
441
  end
@@ -536,6 +540,7 @@ module Samagotchi
536
540
  FileUtils.rm_f(claimed_file)
537
541
  end
538
542
  end
543
+ merged.each { |_prompt, origin| user_input(origin&.dig(:client_id)) }
539
544
  unless merged.empty?
540
545
  @engine.announce(type: :input_merged, count: merged.size, origins: merged.filter_map(&:last))
541
546
  @merged_this_turn.concat(merged)
@@ -628,6 +633,12 @@ module Samagotchi
628
633
  Log.info(:worker, "idle_exit", idle_s: @idle_exit.idle_seconds.round)
629
634
  end
630
635
 
636
+ # A human's input (ArchiveStore.user_input?) un-archives the session;
637
+ # a delegate's, a plugin's or a reminder's doesn't.
638
+ def user_input(client_id)
639
+ ArchiveStore.user_input(@session_id, state_dir: @state_dir) if ArchiveStore.user_input?(client_id)
640
+ end
641
+
631
642
  def stopped_on_disk?
632
643
  SessionManager.stopped_on_disk?(@session_id, state_dir: @state_dir)
633
644
  end
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -123,10 +123,16 @@ files:
123
123
  - docs/releasing.md
124
124
  - docs/sessions.md
125
125
  - lib/samagotchi.rb
126
+ - lib/samagotchi/answer_display.rb
127
+ - lib/samagotchi/archive_store.rb
128
+ - lib/samagotchi/bootstrap/config_writer.rb
129
+ - lib/samagotchi/bootstrap/probe.rb
130
+ - lib/samagotchi/bootstrap_command.rb
126
131
  - lib/samagotchi/bridge.rb
127
132
  - lib/samagotchi/bridge/bounded_queue.rb
128
133
  - lib/samagotchi/bridge/card_store.rb
129
134
  - lib/samagotchi/bridge/event_id.rb
135
+ - lib/samagotchi/bridge/pending_card.rb
130
136
  - lib/samagotchi/bridge/ring_buffer.rb
131
137
  - lib/samagotchi/bridge/sse_writer.rb
132
138
  - lib/samagotchi/bridge/turn_accumulator.rb
@@ -136,6 +142,8 @@ files:
136
142
  - lib/samagotchi/bundle_needs.rb
137
143
  - lib/samagotchi/bundles/btw/manifest.yml
138
144
  - lib/samagotchi/bundles/btw/plugin.rb
145
+ - lib/samagotchi/bundles/check-in/manifest.yml
146
+ - lib/samagotchi/bundles/check-in/plugin.rb
139
147
  - lib/samagotchi/bundles/guardrails/guardrails.md
140
148
  - lib/samagotchi/bundles/guardrails/guardrails/rules.yml
141
149
  - lib/samagotchi/bundles/guardrails/manifest.yml
@@ -146,6 +154,9 @@ files:
146
154
  - lib/samagotchi/bundles/loop-guard/plugin.rb
147
155
  - lib/samagotchi/bundles/mcp/manifest.yml
148
156
  - lib/samagotchi/bundles/mcp/plugin.rb
157
+ - lib/samagotchi/bundles/source-links/hooks/source_links.rb
158
+ - lib/samagotchi/bundles/source-links/manifest.yml
159
+ - lib/samagotchi/bundles/source-links/source_links.md
149
160
  - lib/samagotchi/bundles/system/config_modification_protocol.md
150
161
  - lib/samagotchi/bundles/system/delegated.md
151
162
  - lib/samagotchi/bundles/system/identity.md
@@ -169,6 +180,7 @@ files:
169
180
  - lib/samagotchi/desktop/macos/Info.plist.erb
170
181
  - lib/samagotchi/desktop/macos/Panel.swift
171
182
  - lib/samagotchi/desktop_command.rb
183
+ - lib/samagotchi/empty_answer_retry.rb
172
184
  - lib/samagotchi/engine.rb
173
185
  - lib/samagotchi/guardrails.rb
174
186
  - lib/samagotchi/guardrails/approval.rb
@@ -178,6 +190,7 @@ files:
178
190
  - lib/samagotchi/guardrails/load_failures.rb
179
191
  - lib/samagotchi/guardrails/protected_paths.rb
180
192
  - lib/samagotchi/guardrails/rules.rb
193
+ - lib/samagotchi/guardrails/scratch_writes.rb
181
194
  - lib/samagotchi/guardrails/targets.rb
182
195
  - lib/samagotchi/guardrails/verdict.rb
183
196
  - lib/samagotchi/hooks.rb
@@ -239,15 +252,19 @@ files:
239
252
  - lib/samagotchi/prompt_literal_guard.rb
240
253
  - lib/samagotchi/recap_store.rb
241
254
  - lib/samagotchi/reminder_store.rb
255
+ - lib/samagotchi/reply_wait.rb
256
+ - lib/samagotchi/sampling_settings.rb
242
257
  - lib/samagotchi/self_report.rb
243
258
  - lib/samagotchi/send_command.rb
244
259
  - lib/samagotchi/served_model.rb
245
260
  - lib/samagotchi/session.rb
261
+ - lib/samagotchi/session_archive_command.rb
246
262
  - lib/samagotchi/session_commands.rb
247
263
  - lib/samagotchi/session_delete_command.rb
248
264
  - lib/samagotchi/session_manager.rb
249
265
  - lib/samagotchi/session_metrics.rb
250
266
  - lib/samagotchi/session_observer.rb
267
+ - lib/samagotchi/steer.rb
251
268
  - lib/samagotchi/terminal_ui.rb
252
269
  - lib/samagotchi/terminal_ui/attach_launcher.rb
253
270
  - lib/samagotchi/terminal_ui/attached_loop.rb
@@ -311,6 +328,7 @@ files:
311
328
  - lib/samagotchi/web/markdown_renderer.rb
312
329
  - lib/samagotchi/web/message_parts.rb
313
330
  - lib/samagotchi/web/public/activity.js
331
+ - lib/samagotchi/web/public/annotate_presets.js
314
332
  - lib/samagotchi/web/public/annotations.js
315
333
  - lib/samagotchi/web/public/app.js
316
334
  - lib/samagotchi/web/public/card.js
@@ -327,6 +345,7 @@ files:
327
345
  - lib/samagotchi/web/public/index.html
328
346
  - lib/samagotchi/web/public/init_row.js
329
347
  - lib/samagotchi/web/public/model_pick.js
348
+ - lib/samagotchi/web/public/notify.js
330
349
  - lib/samagotchi/web/public/question_card.js
331
350
  - lib/samagotchi/web/public/route.js
332
351
  - lib/samagotchi/web/public/scope.js