samagotchi 0.62.0 → 0.63.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/README.md +4 -1
  4. data/bin/chi +3 -2
  5. data/docs/cli.md +4 -1
  6. data/docs/configuration.md +1 -1
  7. data/docs/context.md +13 -3
  8. data/docs/hooks.md +6 -3
  9. data/docs/memory.md +1 -1
  10. data/docs/plugins.md +89 -2
  11. data/docs/releasing.md +1 -1
  12. data/docs/sub-agent.md +6 -0
  13. data/docs/testing.md +7 -4
  14. data/lib/samagotchi/answer_refs.rb +18 -26
  15. data/lib/samagotchi/bridge.rb +23 -1
  16. data/lib/samagotchi/bundles/claude-peer/manifest.yml +22 -0
  17. data/lib/samagotchi/bundles/claude-peer/plugin.rb +406 -0
  18. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +104 -0
  19. data/lib/samagotchi/bundles/claude-peer/scripts/pair +66 -0
  20. data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +60 -0
  21. data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
  22. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  23. data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
  24. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  26. data/lib/samagotchi/context_absorber.rb +25 -6
  27. data/lib/samagotchi/idle_recap.rb +2 -3
  28. data/lib/samagotchi/image_store.rb +8 -0
  29. data/lib/samagotchi/plugin/attached_context.rb +7 -4
  30. data/lib/samagotchi/recap_setup.rb +3 -3
  31. data/lib/samagotchi/row_refs.rb +3 -4
  32. data/lib/samagotchi/session.rb +12 -0
  33. data/lib/samagotchi/terminal_ui/attach_launcher.rb +6 -3
  34. data/lib/samagotchi/terminal_ui/attached_loop.rb +26 -9
  35. data/lib/samagotchi/terminal_ui.rb +31 -11
  36. data/lib/samagotchi/tools/execute.rb +22 -4
  37. data/lib/samagotchi/version.rb +1 -1
  38. data/lib/samagotchi/web/app.rb +6 -4
  39. data/lib/samagotchi/web/public/activity.js +2 -2
  40. data/lib/samagotchi/web/public/app.js +47 -91
  41. data/lib/samagotchi/web/public/card.js +73 -23
  42. data/lib/samagotchi/web/public/chains.js +6 -4
  43. data/lib/samagotchi/web/public/command_view.js +39 -35
  44. data/lib/samagotchi/web/public/copy.js +7 -15
  45. data/lib/samagotchi/web/public/diff_view.js +20 -23
  46. data/lib/samagotchi/web/public/format.js +0 -48
  47. data/lib/samagotchi/web/public/images.js +12 -16
  48. data/lib/samagotchi/web/public/question_cards.js +3 -2
  49. data/lib/samagotchi/web/public/read_view.js +10 -9
  50. data/lib/samagotchi/web/public/scope.js +23 -0
  51. data/lib/samagotchi/web/public/timing.js +0 -7
  52. data/lib/samagotchi/web/public/turn_html.js +119 -59
  53. data/lib/samagotchi/web/public/turn_view.js +73 -116
  54. data/lib/samagotchi/web/public/vendor/directive.js +8 -0
  55. data/lib/samagotchi/web/public/vendor/directives/unsafe-html.js +8 -0
  56. data/lib/samagotchi/worker.rb +46 -30
  57. data/lib/samagotchi/worker_inbound.rb +12 -11
  58. metadata +8 -1
@@ -24,13 +24,15 @@ import { html, nothing, render } from "./vendor/lit-html.js";
24
24
  import { setAnswerRefs } from "./answer_refs.js";
25
25
  import { bindRowRefs } from "./refs.js";
26
26
  import { leavesBlock } from "./card.js";
27
- import { copySourceAttr } from "./copy.js";
28
- import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
29
- import { thumbsHtml } from "./images.js";
27
+ import { failedTurnText, normalize, sourceLabel } from "./format.js";
28
+ import { thumbsTemplate } from "./images.js";
30
29
  import { shouldFollowScroll } from "./scroll.js";
31
30
  import { hookNoticeLabel } from "./turn_events.js";
32
- import { commandBubbleHtml, emptyAnswerHtml, htmlElement, shellCommandMessage, toolRowHtml, toolRowInnerHtml, userBubbleHtml } from "./turn_html.js";
33
- import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
31
+ import {
32
+ answerBubbleTemplate, cancelLineTemplate, commandBubbleTemplate, elementOf, emptyAnswerTemplate, noteBubbleTemplate, shellCommandMessage,
33
+ steerRowTemplate, toolRowInnerTemplate, toolRowTemplate, userBubbleTemplate,
34
+ } from "./turn_html.js";
35
+ import { formatDuration, turnGroups, turnTimingText } from "./timing.js";
34
36
  import { createSentenceFeed } from "./sentences.js";
35
37
  import { createTicker } from "./thinking_ticker.js";
36
38
  import * as model from "./turn_model.js";
@@ -110,13 +112,6 @@ function nodesOf(template) {
110
112
  return frag;
111
113
  }
112
114
 
113
- // +markup+ (any HTML string) as nodes.
114
- function rawNodes(markup) {
115
- const t = document.createElement("template");
116
- t.innerHTML = markup;
117
- return t.content;
118
- }
119
-
120
115
  // ---- A finished block's outline ----
121
116
 
122
117
  // A finished block as its outline (turn_model.js outlineRuns of +gens+, its
@@ -170,43 +165,16 @@ export function outlineBlock(blockEl, gens) {
170
165
  return true;
171
166
  }
172
167
 
173
- // Reloaded tool rows (turn_html.js toolRowHtml) as nodes, each row's
168
+ // Reloaded tool rows (turn_html.js toolRowTemplate) as nodes, each row's
174
169
  // plugin refs bound to its title (refs.js bindRowRefs).
175
170
  function rowNodes(rows, thumbs) {
176
- const nodes = rawNodes(rows.map((r) => toolRowHtml(r, { thumbs })).join(""));
171
+ const nodes = nodesOf(rows.map((r) => toolRowTemplate(r, { thumbs })));
177
172
  [...nodes.children].forEach((el, i) => bindRowRefs(el, rows[i]));
178
173
  return nodes;
179
174
  }
180
175
 
181
176
  // ---- The live view ----
182
177
 
183
- // The parts of a drawn tool row a user may have changed (the command's
184
- // raw toggle, an opened diff) or that never change once there (the
185
- // thumbs): a redraw keeps them as they are.
186
- const KEPT_PARTS = ["activity-command", "activity-diff", "activity-read", "thumbs"];
187
-
188
- const childWith = (el, cls) => [...el.children].find((c) => c.classList.contains(cls));
189
-
190
- // A live tool row drawn again from its activity row (status, title,
191
- // output): the reloaded row's markup (toolRowInnerHtml), the kept parts
192
- // and the stop-task button carried over. +html+ is that markup.
193
- function redrawRow(el, markup) {
194
- const fresh = htmlElement(`<div>${markup}</div>`);
195
- for (const cls of KEPT_PARTS) {
196
- const old = childWith(el, cls);
197
- const drawn = childWith(fresh, cls);
198
- if (old && drawn) drawn.replaceWith(old);
199
- }
200
- const stop = childWith(el, "task-stop");
201
- el.replaceChildren(...fresh.childNodes);
202
- if (stop) rowAnchor(el).after(stop);
203
- }
204
-
205
- // Where a row's stop-task button goes: after its title (else its tool).
206
- function rowAnchor(el) {
207
- return childWith(el, "activity-params") || childWith(el, "activity-tool");
208
- }
209
-
210
178
  // The container seams (the stage view composes this view, stage_view.js):
211
179
  // blockHost(el) puts the turn's block in the page (default: the history);
212
180
  // follow() keeps the live part in view (default: the history's
@@ -485,10 +453,11 @@ export function createTurnView({
485
453
  onChange();
486
454
  }
487
455
 
488
- // A tool row is the reloaded row's markup (turn_html.js toolRowHtml),
489
- // drawn again when an event changed it.
490
- const rowHtml = new WeakMap();
491
- const rowThumbs = (images) => thumbsHtml(sessionId(), images);
456
+ // A live tool row is its own element, the reloaded row's template
457
+ // (turn_html.js toolRowInnerTemplate) rendered into it on each event.
458
+ const rowThumbs = (images) => thumbsTemplate(sessionId(), images);
459
+ // A row element → its stop-task button (syncTaskStop).
460
+ const rowStops = new WeakMap();
492
461
 
493
462
  // A row, notice or card into the step's activity; a step that wrote no
494
463
  // narration before it drops the "…".
@@ -504,43 +473,48 @@ export function createTurnView({
504
473
  apply(r);
505
474
  const entry = genEls.get(r.gen);
506
475
  let el = entry.rows.get(r.row.key);
507
- const inner = toolRowInnerHtml(r.row, { thumbs: rowThumbs });
508
476
  if (!el) {
509
- el = htmlElement(toolRowHtml(r.row, { thumbs: rowThumbs }));
510
- bindRowRefs(el, r.row);
511
- rowHtml.set(el, inner);
477
+ el = document.createElement("div");
478
+ el.className = "activity-row";
479
+ el.dataset.key = r.row.key;
512
480
  entry.rows.set(r.row.key, el);
481
+ drawRow(el, r.row);
513
482
  addItem(entry, el);
514
483
  } else {
515
484
  if (!r.gen.text) entry.placeholder = false;
516
- if (rowHtml.get(el) !== inner) {
517
- redrawRow(el, inner);
518
- bindRowRefs(el, r.row);
519
- }
520
- rowHtml.set(el, inner);
485
+ drawRow(el, r.row);
521
486
  // A late result in a closed step: its label and failed mark.
522
487
  draw(entry);
523
488
  }
524
- syncTaskStop(el, r.row);
525
489
  updateBlockSummary();
526
490
  onChange();
527
491
  }
528
492
 
529
- // The stop-task button after a running task_wait's params; gone once the
530
- // row completes.
493
+ // The row rendered again (lit writes only what changed: a toggled raw
494
+ // command, an opened diff or read and the thumbs stay), its stop-task
495
+ // button in its slot, its plugin refs bound to its title (refs.js).
496
+ function drawRow(el, row) {
497
+ render(toolRowInnerTemplate(row, { thumbs: rowThumbs, stop: syncTaskStop(el, row) }), el);
498
+ bindRowRefs(el, row);
499
+ }
500
+
501
+ // The stop-task button of a running task_wait's row (after its title),
502
+ // nothing once the row completes.
531
503
  function syncTaskStop(el, row) {
532
504
  const taskId = taskStop?.target(row);
533
- const btn = el.querySelector(":scope > .task-stop");
505
+ let btn = rowStops.get(el);
534
506
  if (!taskId) {
535
- btn?.remove();
536
- return;
507
+ rowStops.delete(el);
508
+ return nothing;
537
509
  }
538
510
  if (btn) {
539
511
  btn.disabled = taskStop.busy(taskId);
540
- return;
512
+ return btn;
541
513
  }
542
514
  const command = () => taskCommand(turn?.gens, taskId);
543
- rowAnchor(el).after(taskStopButton(taskStop, taskId, { command }));
515
+ btn = taskStopButton(taskStop, taskId, { command });
516
+ rowStops.set(el, btn);
517
+ return btn;
544
518
  }
545
519
 
546
520
  // A hook's line (hook_notice) as a row of the current step, above the row
@@ -560,7 +534,7 @@ export function createTurnView({
560
534
  // that ends first puts it in its last step). False with no running turn.
561
535
  function steer(data) {
562
536
  if (!turn || turn.ended) return false;
563
- pendingSteers.push(htmlElement(steerRowHtml(data)));
537
+ pendingSteers.push(elementOf(steerRowTemplate(data)));
564
538
  return true;
565
539
  }
566
540
 
@@ -829,26 +803,12 @@ export function stepText(block, iteration) {
829
803
  // A re-rendered failed turn's line (a .bubble.cancel, as live's
830
804
  // addEndBubble), in turn_failed's words (failedTurnText) from what the turn
831
805
  // record kept of it (SessionMetrics' failure: summary, message,
832
- // error_class, kept_steps); "" for any other record, or a failed one saved
833
- // before records kept that.
834
- function failedLineHtml(record) {
806
+ // error_class, kept_steps); nothing for any other record, or a failed one
807
+ // saved before records kept that.
808
+ function failedLineTemplate(record) {
835
809
  const failure = record?.status === "failed" ? record.failure : null;
836
- if (!failure || typeof failure !== "object") return "";
837
- return `<div class="bubble cancel">${escapeHtml(failedTurnText(failure))}</div>`;
838
- }
839
-
840
- // Parts (HTML strings and templates, in order) as one fragment.
841
- function fragmentOf(parts) {
842
- const out = document.createDocumentFragment();
843
- let markup = "";
844
- for (const part of parts) {
845
- if (typeof part === "string") { markup += part; continue; }
846
- if (markup) out.append(rawNodes(markup));
847
- markup = "";
848
- out.append(nodesOf(part));
849
- }
850
- if (markup) out.append(rawNodes(markup));
851
- return out;
810
+ if (!failure || typeof failure !== "object") return nothing;
811
+ return html`<div class="bubble cancel">${failedTurnText(failure)}</div>`;
852
812
  }
853
813
 
854
814
  // A reloaded history in the turn view (renderHistory's object path), as
@@ -857,53 +817,51 @@ function fragmentOf(parts) {
857
817
  // the thinking, params and output, from the saved messages; status and
858
818
  // duration from the timing's tool records), then the answer and the turn's
859
819
  // timing line under it.
860
- // +thumbs(images)+ draws a prompt's or a tool row's image thumbs.
820
+ // +thumbs(images)+ draws a prompt's or a tool row's image thumbs (a
821
+ // template, images.js thumbsTemplate).
861
822
  export function turnHistoryNodes(items, timing, { thumbs }) {
862
- const timingHtml = (group) => {
823
+ const timingTemplate = (group) => {
863
824
  const record = group.record;
864
825
  // Numbered by its place in the turn records, as live (a failed turn
865
826
  // whose prompt went back still counts).
866
827
  const at = (timing?.turnRecords || []).indexOf(record);
867
828
  return record && formatDuration(record.duration_ms)
868
- ? `<div class="turn-timing">${escapeHtml(turnTimingText(at >= 0 ? at + 1 : group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" }))}</div>`
869
- : "";
829
+ ? html`<div class="turn-timing">${turnTimingText(at >= 0 ? at + 1 : group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" })}</div>`
830
+ : nothing;
870
831
  };
871
- // The timing line after the bubble, not in it: where the live view
872
- // leaves it at the turn's end. +at+: the message's index, for its refs
832
+ // An answer's bubble; +at+, the message's index, is for its refs
873
833
  // (data-answer-at, gone before the nodes are returned).
874
- const answerHtml = (at, timing) => {
875
- const m = items[at];
876
- const { body, renderedMarkdown } = messageBodyHtml(m);
877
- return `<div class="bubble output${renderedMarkdown ? " markdown" : ""}" data-answer-at="${at}"${copySourceAttr(m, renderedMarkdown)}>${body}</div>${timing}`;
878
- };
834
+ const answerBubble = (at, cls = "") => answerBubbleTemplate(items[at], { cls, at });
835
+ // The timing line after the bubble, not in it: where the live view
836
+ // leaves it at the turn's end.
837
+ const answerTemplate = (at, timing) => html`${answerBubble(at)}${timing}`;
879
838
  // A user's line, labelled when chi brought it in (a delegate report). A
880
839
  // `!cmd`'s saved message (the worker's "!(<command>)\n<output>") renders
881
840
  // as its command bubble, as live does — not as a user bubble.
882
- const userHtml = (m, opts = {}) => {
841
+ const userTemplate = (m, opts = {}) => {
883
842
  const shell = shellCommandMessage(m?.content);
884
- if (shell) return commandBubbleHtml(shell);
885
- return userBubbleHtml(m, { label: sourceLabel(m?.source), thumbs: thumbs(m.images), ...opts });
843
+ if (shell) return commandBubbleTemplate(shell);
844
+ return userBubbleTemplate(m, { label: sourceLabel(m?.source), thumbs: thumbs(m.images), ...opts });
886
845
  };
887
846
  // The first text after a delegate report merged mid-turn, unless it is
888
847
  // the turn's answer (shown anyway).
889
- const reportReplyHtml = (i, group) => {
890
- if (items[i]?.source !== "delegate_report") return "";
848
+ const reportReplyTemplate = (i, group) => {
849
+ if (items[i]?.source !== "delegate_report") return nothing;
891
850
  const j = items.findIndex((m, k) => k > i && m?.role === "assistant" && String(m.content ?? "").trim());
892
- if (j < 0 || j === group.answer || items.slice(i + 1, j).some((m) => m?.role === "user" && !m.merged)) return "";
893
- const { body, renderedMarkdown } = messageBodyHtml(items[j]);
894
- return `<div class="bubble output report-reply${renderedMarkdown ? " markdown" : ""}" data-answer-at="${j}"${copySourceAttr(items[j], renderedMarkdown)}>${body}</div>`;
851
+ if (j < 0 || j === group.answer || items.slice(i + 1, j).some((m) => m?.role === "user" && !m.merged)) return nothing;
852
+ return answerBubble(j, " report-reply");
895
853
  };
896
- const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
854
+ const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
897
855
  const parts = [];
898
856
  // Each block's steps, in order: its outline once the nodes exist.
899
857
  const outlines = [];
900
858
  for (const group of turnGroups(items, timing)) {
901
- if (group.kind === "note") { parts.push(`<div class="bubble note">${noteHtml(items[group.i])}</div>`); continue; }
902
- if (group.kind === "steer") { parts.push(steerHtml(group.i, true)); continue; }
903
- if (group.kind === "answer") { parts.push(answerHtml(group.answer, "")); continue; }
859
+ if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
860
+ if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
861
+ if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
904
862
  const prompt = items[group.user];
905
863
  // A context wake turn starts with its note.
906
- parts.push(prompt.role === "note" ? `<div class="bubble note">${noteHtml(prompt)}</div>` : userHtml(prompt));
864
+ parts.push(prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt));
907
865
  const [only] = group.steps;
908
866
  const ended = group.answer !== null || group.emptyAnswer != null;
909
867
  const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
@@ -917,7 +875,7 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
917
875
  const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
918
876
  const steps = gens.map((gen, k) => stepTemplate(gen, {
919
877
  // The steers the step answered first, where the live rows went.
920
- steers: (group.steps[k].steers || []).map((i) => rawNodes(steerHtml(i, false))),
878
+ steers: (group.steps[k].steers || []).map((i) => steerTemplate(i, false)),
921
879
  items: gen.tools.length ? [rowNodes(gen.tools, thumbs)] : [],
922
880
  }));
923
881
  parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
@@ -926,28 +884,27 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
926
884
  // Steers with no step to sit in, or in a turn that shows no block: on
927
885
  // their own after the prompt.
928
886
  const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
929
- parts.push(loose.map((i) => steerHtml(i, true)).join(""));
887
+ parts.push(loose.map((i) => steerTemplate(i, true)));
930
888
  // The user's lines merged into the turn: the live "steered" bubbles,
931
889
  // after its steps and before its answer. A delegate report's is
932
890
  // followed by the text that answered it when that isn't the answer: a
933
891
  // small model tells its user about a report before its next tool call,
934
892
  // and that step's text would stay folded in the block.
935
893
  parts.push((group.merged || []).map((i) =>
936
- userHtml(items[i], { state: "steered", step: items[i].step }) +
937
- reportReplyHtml(i, group)).join(""));
894
+ html`${userTemplate(items[i], { state: "steered", step: items[i].step })}${reportReplyTemplate(i, group)}`));
938
895
  // A turn that ended with no answer: chi's muted notice where the answer
939
896
  // would be, before the timing, as live.
940
- if (group.emptyAnswer != null) parts.push(`${emptyAnswerHtml(items[group.emptyAnswer])}${timingHtml(group)}`);
897
+ if (group.emptyAnswer != null) parts.push(html`${emptyAnswerTemplate(items[group.emptyAnswer])}${timingTemplate(group)}`);
941
898
  // A turn with no answer (canceled, or running: no record yet) shows its
942
899
  // timing after its prompt and steps, where the live view leaves it.
943
- else if (group.answer === null) parts.push(timingHtml(group));
944
- else parts.push(answerHtml(group.answer, timingHtml(group)));
900
+ else if (group.answer === null) parts.push(timingTemplate(group));
901
+ else parts.push(answerTemplate(group.answer, timingTemplate(group)));
945
902
  // A canceled turn ends with its cancel line, a failed one whose steps
946
903
  // stayed with its failure line, as the live view left them.
947
- parts.push(cancelLineHtml(group.record, escapeHtml));
948
- parts.push(failedLineHtml(group.record));
904
+ parts.push(cancelLineTemplate(group.record));
905
+ parts.push(failedLineTemplate(group.record));
949
906
  }
950
- const out = fragmentOf(parts);
907
+ const out = nodesOf(parts);
951
908
  // A finished turn's block as the live end leaves it: its outline, open.
952
909
  [...out.children].filter((el) => el.matches(".turn-work")).forEach((block, i) => outlineBlock(block, outlines[i] || []));
953
910
  // Each answer bubble knows its message's refs (answer_refs.js; the
@@ -0,0 +1,8 @@
1
+ // lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directive.js: the minified production
2
+ // build, unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
3
+ /**
4
+ * @license
5
+ * Copyright 2017 Google LLC
6
+ * SPDX-License-Identifier: BSD-3-Clause
7
+ */
8
+ const t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}export{i as Directive,t as PartType,e as directive};
@@ -0,0 +1,8 @@
1
+ // lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directives/unsafe-html.js: the minified
2
+ // production build, unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
3
+ import{nothing as t,noChange as i}from"../lit-html.js";import{directive as r,Directive as s,PartType as n}from"../directive.js";
4
+ /**
5
+ * @license
6
+ * Copyright 2017 Google LLC
7
+ * SPDX-License-Identifier: BSD-3-Clause
8
+ */class e extends s{constructor(i){if(super(i),this.it=t,i.type!==n.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(r){if(r===t||null==r)return this._t=void 0,this.it=r;if(r===i)return r;if("string"!=typeof r)throw Error(this.constructor.directiveName+"() called with a non-string value");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1;const o=r(e);export{e as UnsafeHTMLDirective,o as unsafeHTML};
@@ -468,10 +468,10 @@ module Samagotchi
468
468
  # would keep the hub's "needs you" badge on for good. Dropped and saved
469
469
  # before the Engine and the Bridge exist (no question lock, no events).
470
470
  def drop_dead_question
471
- return unless @session.pending_question
471
+ id = @session.drop_dead_question
472
+ return unless id
472
473
 
473
- Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
474
- @session.pending_question = nil
474
+ Log.info(:worker, "dead_question_dropped", id: id)
475
475
  save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
476
476
  end
477
477
 
@@ -1103,22 +1103,36 @@ module Samagotchi
1103
1103
  end
1104
1104
 
1105
1105
  # Check again with the event log held, which the Bridge holds while it
1106
- # queues a POST /turn, then close the Bridge so no client can queue one
1107
- # after the check, and stop the idle jobs (reminder callback, recap).
1106
+ # queues a POST /turn, then close the Bridge's intake so no client can
1107
+ # queue one after the check, and stop the idle jobs (reminder callback,
1108
+ # recap). The Bridge stops after the log is let go (#stop_bridge_after).
1108
1109
  # A client connecting from here on finds no worker: `chi --attach` fails
1109
1110
  # and the web stream answers 503 (a small window, left as is).
1110
1111
  # @return [Boolean] false when something came in since #due?
1111
1112
  def leave_idle
1112
- @engine.synchronize_events do
1113
- next false unless @idle_exit.due?
1114
-
1115
- @bridge&.stop
1116
- @engine.stop_idle
1117
- log_idle_exit
1118
- true
1113
+ stop_bridge_after do
1114
+ @engine.synchronize_events do
1115
+ next false unless @idle_exit.due?
1116
+
1117
+ @bridge&.close_intake
1118
+ @engine.stop_idle
1119
+ log_idle_exit
1120
+ true
1121
+ end
1119
1122
  end
1120
1123
  end
1121
1124
 
1125
+ # The Bridge stops once the block (which closed its intake with the
1126
+ # event log held) answers true, outside the log: Bridge#stop waits for
1127
+ # the requests it is answering, and one waiting on the log would hold
1128
+ # it for the whole grace before being killed.
1129
+ # @return [Boolean] what the block answered
1130
+ def stop_bridge_after
1131
+ leaving = yield
1132
+ @bridge&.stop if leaving
1133
+ leaving
1134
+ end
1135
+
1122
1136
  # Bridge POST /exit, on a Bridge thread with the event log held.
1123
1137
  # +delete+: the session is deleted after (/exit --delete), so no recap.
1124
1138
  # +restart+: a new worker takes over (on the newest chi installed), so
@@ -1140,29 +1154,31 @@ module Samagotchi
1140
1154
  end
1141
1155
 
1142
1156
  # #leave_idle for an exit a client asked for: check again with the event
1143
- # log held (a prompt may have come in since), then close the Bridge and
1144
- # stop the idle jobs. Other clients' streams no longer hold: the asker's
1157
+ # log held (a prompt may have come in since), then close the Bridge's
1158
+ # intake and stop the idle jobs (the Bridge stops after the log). Other clients' streams no longer hold: the asker's
1145
1159
  # may not have closed yet, and a UI joining now finds the worker gone
1146
1160
  # (:stream_closed), the window #leave_idle has too.
1147
1161
  # @return [Boolean] false when something came in since the request
1148
1162
  def leave_on_request
1149
- @engine.synchronize_events do
1150
- hold = if @exit_restart
1151
- @idle_exit.hold_for_restart
1152
- else
1153
- @idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
1154
- end
1155
- if hold
1156
- @exit_requested = nil
1157
- @exit_restart = false
1158
- Log.info(:worker, "exit_held", reason: hold)
1159
- next false
1160
- end
1163
+ stop_bridge_after do
1164
+ @engine.synchronize_events do
1165
+ hold = if @exit_restart
1166
+ @idle_exit.hold_for_restart
1167
+ else
1168
+ @idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
1169
+ end
1170
+ if hold
1171
+ @exit_requested = nil
1172
+ @exit_restart = false
1173
+ Log.info(:worker, "exit_held", reason: hold)
1174
+ next false
1175
+ end
1161
1176
 
1162
- @bridge&.stop
1163
- @engine.stop_idle
1164
- Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
1165
- true
1177
+ @bridge&.close_intake
1178
+ @engine.stop_idle
1179
+ Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
1180
+ true
1181
+ end
1166
1182
  end
1167
1183
  end
1168
1184
 
@@ -106,9 +106,10 @@ module Samagotchi
106
106
  # run): auto-attached context alone doesn't make a session worth
107
107
  # keeping. Saved, then the subscriptions written (a crash between the
108
108
  # two re-delivers; the note ids dedupe).
109
- # +wake+: the session is idle with nothing queued, so an update whose
110
- # source asked to wake may start a turn (#context_wake_for): its note
111
- # goes in as the turn's first message.
109
+ # +wake+: the session is idle with nothing queued, so a delivery with a
110
+ # wake note (an update whose source asked to wake, or a session push
111
+ # source's first text that asked) may start a turn (#context_wake_for):
112
+ # its wake note goes in as the turn's first message.
112
113
  # @return [ContextWake, nil] the wake turn to run now
113
114
  # @raise [SystemCallError, IOError] the caller logs it (context_failed)
114
115
  def absorb_context(before_turn: false, wake: false)
@@ -134,7 +135,7 @@ module Samagotchi
134
135
  waking && ContextWake.new(delivery: waking, turn_id: turn_id)
135
136
  end
136
137
 
137
- # A failed wake turn kept nothing but its note: the update goes back to
138
+ # A failed wake turn kept nothing but its note: the note goes back to
138
139
  # a plain note (no turn start, the background wording), so a reload
139
140
  # draws no empty turn for it.
140
141
  def unmark_wake(context_wake)
@@ -224,13 +225,13 @@ module Samagotchi
224
225
  true
225
226
  end
226
227
 
227
- # The update in +batch+ that may start a wake turn now: its source
228
- # asked (wake: true), context.wake is on, the wake budget is open
229
- # (WorkerWakes#context_open?: no continue offer, not paused, under
230
- # session.max_wakes, past the start grace) and the source hasn't woken
231
- # the session in the last CONTEXT_WAKE_WINDOW. The others stay plain
232
- # notes. Within the start grace a change is a note: it came while the
233
- # session was away.
228
+ # The delivery in +batch+ that may start a wake turn now: it has a wake
229
+ # note (ContextAbsorber decides: its source asked, wake: true),
230
+ # context.wake is on, the wake budget is open (WorkerWakes#context_open?:
231
+ # no continue offer, not paused, under session.max_wakes, past the start
232
+ # grace) and the source hasn't woken the session in the last
233
+ # CONTEXT_WAKE_WINDOW. The others stay plain notes. Within the start
234
+ # grace a change is a note: it came while the session was away.
234
235
  def context_wake_for(batch, now: Time.now)
235
236
  candidates = batch.deliveries.select(&:wake_note)
236
237
  return nil if candidates.empty? || !context_wakes_on?
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.62.0
4
+ version: 0.63.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -185,6 +185,11 @@ files:
185
185
  - lib/samagotchi/bundles/btw/plugin.rb
186
186
  - lib/samagotchi/bundles/check-in/manifest.yml
187
187
  - lib/samagotchi/bundles/check-in/plugin.rb
188
+ - lib/samagotchi/bundles/claude-peer/manifest.yml
189
+ - lib/samagotchi/bundles/claude-peer/plugin.rb
190
+ - lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md
191
+ - lib/samagotchi/bundles/claude-peer/scripts/pair
192
+ - lib/samagotchi/bundles/claude-peer/skill_claude_peer.md
188
193
  - lib/samagotchi/bundles/coordinator/manifest.yml
189
194
  - lib/samagotchi/bundles/coordinator/plugin.rb
190
195
  - lib/samagotchi/bundles/coordinator/skill_coordinator.md
@@ -600,6 +605,8 @@ files:
600
605
  - lib/samagotchi/web/public/turn_model.js
601
606
  - lib/samagotchi/web/public/turn_view.js
602
607
  - lib/samagotchi/web/public/update_notice.js
608
+ - lib/samagotchi/web/public/vendor/directive.js
609
+ - lib/samagotchi/web/public/vendor/directives/unsafe-html.js
603
610
  - lib/samagotchi/web/public/vendor/lit-html.js
604
611
  - lib/samagotchi/web/qr.rb
605
612
  - lib/samagotchi/web/server.rb