samagotchi 0.156.0 → 0.158.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -28,19 +28,24 @@ import { html, nothing, render } from "./vendor/lit-html.js";
28
28
  import { answerRefsOf, setAnswerRefs } from "./answer_refs.js";
29
29
  import { bindRowRefs } from "./refs.js";
30
30
  import { openAfterEnd } from "./block_open.js";
31
- import { leavesBlock } from "./card.js";
32
- import { failedTurnText, normalize, sourceLabel } from "./format.js";
31
+ import { drawCard, isNoticeCard, leavesBlock, shownElsewhere } from "./card.js";
32
+ import { routeChunk } from "./chunk_router.js";
33
+ import { drawCommandBubble } from "./commands.js";
34
+ import { endBubbleTemplate } from "./bubbles.js";
35
+ import { drawnKind } from "./turn_drawing.js";
36
+ import { failedTurnText, fullTime, normalize, sourceLabel } from "./format.js";
33
37
  import { thumbsTemplate } from "./images.js";
34
38
  import { shouldFollowScroll } from "./scroll.js";
35
- import { hookNoticeLabel } from "./turn_events.js";
39
+ import { emptyRetryLine, hookNoticeLabel, llmContextNotice, reminderText, steerCutLine } from "./turn_events.js";
36
40
  import {
37
41
  answerBubbleTemplate, cancelLineTemplate, commandBubbleTemplate, elementOf, emptyAnswerTemplate, noteBubbleTemplate, shellCommandMessage,
38
42
  steerRowTemplate, toolRowInnerTemplate, toolRowTemplate, userBubbleTemplate,
39
43
  } from "./turn_html.js";
40
- import { formatDuration, turnGroups, turnTimingText } from "./timing.js";
44
+ import { cancelLineText, formatDuration, turnGroups, turnTimingText } from "./timing.js";
41
45
  import { createSentenceFeed } from "./sentences.js";
42
46
  import { createTicker } from "./thinking_ticker.js";
43
47
  import * as model from "./turn_model.js";
48
+ import { answerGen, turnPieces } from "./turn_pieces.js";
44
49
  import { linkedLine } from "./notice_links.js";
45
50
  import { taskCommand, taskStopButton } from "./task_stop.js";
46
51
  import { footprintTemplate, groupItems, stampHistory } from "./working_set_view.js";
@@ -212,6 +217,7 @@ export function createTurnBlock({
212
217
  onChange = () => {},
213
218
  onThinkingLine = () => {},
214
219
  taskStop = null,
220
+ thumbs = null,
215
221
  }) {
216
222
  let turn = null;
217
223
  let blockEl = null;
@@ -482,7 +488,7 @@ export function createTurnBlock({
482
488
 
483
489
  // A live tool row is its own element, the reloaded row's template
484
490
  // (turn_html.js toolRowInnerTemplate) rendered into it on each event.
485
- const rowThumbs = (images) => thumbsTemplate(sessionId(), images);
491
+ const rowThumbs = thumbs || ((images) => thumbsTemplate(sessionId(), images));
486
492
  // A row element → its stop-task button (syncTaskStop).
487
493
  const rowStops = new WeakMap();
488
494
 
@@ -643,14 +649,13 @@ export function createTurnBlock({
643
649
  function turnEnded({ kind, endNote = null, turnId = null }) {
644
650
  // Once per turn: a second end (a re-dispatched event) would promote again.
645
651
  if (!turn || turn.ended) return [];
646
- const last = model.currentGen(turn);
647
652
  // A completed turn's answer is its last generation's text; a one-step
648
653
  // turn without tools leaves the block in any case (canceled, it is the
649
- // partial text bubble).
650
- const promoting = last && (kind === "completed" || (turn.gens.length === 1 && last.tools.length === 0));
654
+ // partial text bubble): turn_pieces.js answerGen.
655
+ const last = answerGen(turn, kind);
651
656
  if (pendingSteers.length) flushSteers();
652
657
  apply(model.turnEnded(turn));
653
- if (promoting) promote(last, { endNote, turnId: kind === "completed" ? turnId : null });
658
+ if (last) promote(last, { endNote, turnId: kind === "completed" ? turnId : null });
654
659
  else if (endNote) placeAnswer([endNote], { plain: false, blockEl });
655
660
  const kept = [];
656
661
  if (blockEl) {
@@ -819,6 +824,140 @@ function failedLineTemplate(record) {
819
824
  return html`<div class="bubble cancel">${failedTurnText(failure)}</div>`;
820
825
  }
821
826
 
827
+ // ---- A drawn turn ----
828
+
829
+ // A drawn turn's card: the snapshot's card of its id when the page has it
830
+ // (+cards+: CardStore's, its latest version, an update after the turn too,
831
+ // as live shows it), else the drawing's with its buttons off. Where it
832
+ // goes is the drawing's (drawnTurnNodes).
833
+ function drawnCardEl(event, cards) {
834
+ const stored = (cards || []).find((c) => c?.type === "card" && String(c.id) === String(event.id));
835
+ const card = stored ? { ...stored, in_turn: true } : event;
836
+ const el = drawCard(document.createElement(isNoticeCard(card) ? "details" : "div"), card);
837
+ // The drawing's own copy (no CardStore card: capped out, or its file
838
+ // gone) can't tell whether its buttons still mean anything: off. A
839
+ // CardStore card's buttons are as CardStore lists them (it drops the
840
+ // dead ones itself: an earlier worker's, a turn that left nothing), as
841
+ // live leaves them.
842
+ if (!stored) el.querySelectorAll(".card-action").forEach((button) => { button.disabled = true; });
843
+ return el;
844
+ }
845
+
846
+ // A drawn turn's pieces (+group+: turnGroups' drawn group, its drawing
847
+ // {events, end}), drawn by the live turn block itself: its events go
848
+ // through the block's own handlers as the stream handlers send them live
849
+ // (stream_handlers.js, the stage's view), so the steps, their rows,
850
+ // notices, steers and in-turn cards are where live put them, then the
851
+ // turn ends as live ended it. What live put beside the block (the stage's
852
+ // extras: a reminder's bubble, a merged line's, a card that asks or
853
+ // warns) comes back in arrival order. A merged line and the answer are
854
+ // drawn from their saved items (their markdown, refs, keys), the steps'
855
+ // texts take their items' markdown and refs.
856
+ // @return {block, extras: [{el} | {merged: index}], answer: [nodes], end, kept: [nodes]}
857
+ function drawnTurnNodes(group, items, { thumbs, capabilities = {}, cards = null }) {
858
+ const { events = [], end = {} } = group.drawing || {};
859
+ const kind = drawnKind(end);
860
+ let blockEl = null;
861
+ const answer = [];
862
+ const extras = [];
863
+ const view = createTurnBlock({
864
+ historyEl: document.createElement("div"), isNearBottom: () => false, sawText: () => {}, sessionId: () => null,
865
+ thumbs: (images) => thumbs(images), follow: () => {},
866
+ blockHost: (el) => { blockEl = el; },
867
+ placeAnswer: (pieces, { plain }) => {
868
+ if (plain) blockEl = null;
869
+ answer.push(...pieces);
870
+ },
871
+ });
872
+ const merged = [...(group.merged || [])];
873
+ // The ✂ marks a strategy saved on the turn's outputs since (its items'
874
+ // marks, by output id): an overlay on the rows, as a reload's.
875
+ const marks = Object.assign({}, ...items.filter((it) => it?.drawn === group.drawn && it.marks).map((it) => it.marks));
876
+ for (const event of events) {
877
+ switch (event?.type) {
878
+ case "turn_started": view.turnStarted(event); break;
879
+ case "generation_started": view.generationStarted(event, null); break;
880
+ case "generation_chunk": view.chunk({ ...routeChunk(event), iteration: event.iteration ?? null }); break;
881
+ case "generation_completed": view.generationCompleted(); break;
882
+ case "tool_call_started": view.toolStarted(event); break;
883
+ case "tool_call_completed": view.toolCompleted(marks[event.tool_id] ? { ...event, edit: marks[event.tool_id] } : event); break;
884
+ case "hook_notice": if (!shownElsewhere(event, capabilities)) view.notice(event); break;
885
+ case "empty_answer_retry": view.notice({ line: emptyRetryLine(event) }); break;
886
+ case "steer_cut": view.notice({ line: steerCutLine(event), cls: "steer-cut" }); break;
887
+ case "llm_context_edited": view.notice(llmContextNotice(event)); break;
888
+ case "pending_input_merged": (event.steers || []).forEach((steer) => view.steer(steer)); break;
889
+ case "merged_input": extras.push({ merged: merged.shift() ?? null }); break;
890
+ case "reminder_injected": {
891
+ const el = document.createElement("div");
892
+ drawCommandBubble(el, { label: null, line: reminderText(event), text: "" });
893
+ const when = fullTime(event.at);
894
+ if (when) el.title = when;
895
+ extras.push({ el });
896
+ break;
897
+ }
898
+ case "card": {
899
+ // Live: a card that asks (its actions run commands) or warns stays
900
+ // in sight beside the block (cards_place.js upsertCard, the
901
+ // stage's card); any other is a row of its step.
902
+ const el = drawnCardEl(event, cards);
903
+ const asks = (event.actions || []).some((a) => a && a.command);
904
+ const warn = el.classList.contains("warn");
905
+ if (asks || warn || !view.card(el)) extras.push({ el, warn });
906
+ break;
907
+ }
908
+ default: break;
909
+ }
910
+ }
911
+ const empty = kind === "completed" && (group.emptyAnswer != null || end.turn_summary?.empty_answer);
912
+ const endNote = empty ? elementOf(emptyAnswerTemplate(group.emptyAnswer != null ? items[group.emptyAnswer] : end.turn_summary.empty_answer)) : null;
913
+ const kept = view.turnEnded({ kind, endNote, turnId: end.turn_id || group.drawn }) || [];
914
+ if (endNote && !answer.includes(endNote)) answer.push(endNote);
915
+ // The stage's turn end: the warn cards beside the block leave with the
916
+ // block's, after the end line.
917
+ kept.push(...extras.filter((x) => x.warn).map((x) => x.el));
918
+ const steps = blockEl ? [...blockEl.querySelectorAll(":scope > .gen > .gen-text")] : [];
919
+ pairStepTexts(steps, group, items);
920
+ return {
921
+ block: blockEl,
922
+ extras: extras.filter((x) => !x.warn),
923
+ answer: answer.map((el) => (el.matches?.(".bubble.output") ? savedAnswer(el, group, items, kind) : el)),
924
+ end: kind === "canceled" ? endBubbleTemplate(cancelLineText(end.cancellation_reason || "", end.cancelled_by), { stopped: true })
925
+ : kind === "failed" ? endBubbleTemplate(failedTurnText(end)) : null,
926
+ kept,
927
+ };
928
+ }
929
+
930
+ // The live answer bubble as the reload draws it from its saved message
931
+ // (its markdown, refs, display: answerBubbleTemplate), when one of the
932
+ // turn's texts says what it says; else as live drew it.
933
+ function savedAnswer(el, group, items, kind) {
934
+ const text = el.textContent.trim();
935
+ const at = [group.answer, ...[...(group.texts || [])].reverse()].find((i) => i != null && String(items[i]?.content ?? "").trim() === text);
936
+ if (at == null) return el;
937
+ return nodesOf(answerBubbleTemplate(items[at], { at, turnId: kind === "completed" ? group.turnId : null })).firstElementChild;
938
+ }
939
+
940
+ // The steps' texts paired with the turn's saved texts in order: each its
941
+ // message's index (data-answer-at: its refs) and its rendered markdown,
942
+ // as a reloaded step's (textTemplate).
943
+ function pairStepTexts(els, group, items) {
944
+ const texts = (group.texts || []).filter((i) => i !== group.answer);
945
+ let k = 0;
946
+ for (const el of els) {
947
+ const text = el.textContent.trim();
948
+ const found = texts.slice(k).findIndex((i) => String(items[i]?.content ?? "").trim() === text);
949
+ if (found < 0) continue;
950
+ const at = texts[k + found];
951
+ k += found + 1;
952
+ const rendered = items[at]?.html;
953
+ if (typeof rendered === "string" && rendered) {
954
+ el.replaceWith(nodesOf(textTemplate(String(items[at].content), { live: false, at, rendered })).firstElementChild);
955
+ } else {
956
+ el.dataset.answerAt = String(at);
957
+ }
958
+ }
959
+ }
960
+
822
961
  // A reloaded history as turn blocks (renderHistory's object path), as
823
962
  // nodes: per turn the prompt, a collapsed block of its steps when it had
824
963
  // any (stepTemplate, the live page's: texts, and with the server's parts
@@ -830,7 +969,11 @@ function failedLineTemplate(record) {
830
969
  // web.working_set): each block stamped with its
831
970
  // turn's ordinal and its rows with theirs (working_set_view.js), and the
832
971
  // turn's footprint on its summary line.
833
- export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now() }) {
972
+ // +drawings+ (chi web's turn_drawings): a turn stamped with a drawn id is
973
+ // drawn from its drawing (drawnTurnNodes), its pieces marked data-drawn;
974
+ // +cards+ the snapshot's cards (a drawn card's body), +capabilities+ the
975
+ // page's (a notice shown elsewhere).
976
+ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now(), drawings = null, cards = null, capabilities = {} }) {
834
977
  const timingTemplate = (group) => {
835
978
  const record = group.record;
836
979
  // Numbered by its place in the turn records, as live (a failed turn
@@ -868,6 +1011,38 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
868
1011
  return answerBubble(j, " report-reply", null, replyKey);
869
1012
  };
870
1013
  const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
1014
+ // A drawn turn's pieces (drawnTurnNodes), in the hand-off's order, each
1015
+ // top-level element marked with its turn (data-drawn: cards_place.js
1016
+ // leaves its in-turn entries alone, the reminder dedupe finds it).
1017
+ const drawnGroupPieces = (group, ordinal) => {
1018
+ const prompt = group.user != null ? items[group.user] : null;
1019
+ const drawn = drawnTurnNodes(group, items, { thumbs, capabilities, cards });
1020
+ if (drawn.block) {
1021
+ if (workingSet) drawn.block.querySelector(":scope > summary > .tw-sum")?.prepend(nodesOf(footprintTemplate(groupItems(group, ordinal), ordinal)));
1022
+ outlines.push(null);
1023
+ blockEnds.push(null);
1024
+ blockTurns.push(ordinal);
1025
+ }
1026
+ const lineKey = (i) => (items[i]?.source === "delegate_report" ? `${group.turnId}:input:${i - (group.user ?? i)}` : null);
1027
+ const reviewer = group.answer !== null && prompt?.turn_id && prompt.participant_mode !== PAIR ? authorBadge(prompt.author, prompt.author_name) : null;
1028
+ const pieces = turnPieces({
1029
+ prompt: prompt ? (prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt)) : null,
1030
+ block: drawn.block,
1031
+ merged: drawn.extras.map((x) => (x.el ? x.el : x.merged != null
1032
+ ? html`${userTemplate(items[x.merged], { state: items[x.merged].merged ? "steered" : null, step: items[x.merged].step, lineKey: lineKey(x.merged) })}${reportReplyTemplate(x.merged, group)}`
1033
+ : null)),
1034
+ answer: drawn.answer,
1035
+ timing: timingTemplate(group),
1036
+ apply: reviewer ? applyRowTemplate(prompt.turn_id, reviewer) : null,
1037
+ end: drawn.end,
1038
+ kept: drawn.kept,
1039
+ });
1040
+ return pieces.flatMap((piece) => {
1041
+ const els = piece?.nodeType === 1 ? [piece] : [...nodesOf(piece).children];
1042
+ els.forEach((el) => { el.dataset.drawn = group.drawn; });
1043
+ return els;
1044
+ });
1045
+ };
871
1046
  const parts = [];
872
1047
  // Each block's steps, in order: its outline once the nodes exist; how its
873
1048
  // turn ended (whether it stays open); and its turn's place among the turns
@@ -876,7 +1051,7 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
876
1051
  const blockEnds = [];
877
1052
  const blockTurns = [];
878
1053
  let turnOrdinal = -1;
879
- const groups = turnGroups(items, timing);
1054
+ const groups = turnGroups(items, timing, { drawings });
880
1055
  // A quiet stretch before a group: its divider first (time_gaps.js).
881
1056
  const gaps = groupGaps(groups, items, now);
882
1057
  for (const [k, group] of groups.entries()) {
@@ -887,9 +1062,13 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
887
1062
  // A `!cmd`'s record: its command bubble, no steps, no timing.
888
1063
  if (group.kind === "command") { parts.push(userTemplate(items[group.i])); continue; }
889
1064
  turnOrdinal += 1;
1065
+ if (group.drawn) {
1066
+ parts.push(drawnGroupPieces(group, turnOrdinal));
1067
+ continue;
1068
+ }
890
1069
  const prompt = items[group.user];
891
1070
  // A context wake turn starts with its note.
892
- parts.push(prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt));
1071
+ const promptPiece = prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt);
893
1072
  const [only] = group.steps;
894
1073
  const ended = group.answer !== null || group.emptyAnswer != null;
895
1074
  const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
@@ -897,8 +1076,9 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
897
1076
  // none): no block, the plain answer's collapsed thinking block.
898
1077
  const plainThinking = group.steps.length === 1 && ended && textless && !only.tools.length && only.thinking;
899
1078
  const noBlock = !group.steps.length || plainThinking;
1079
+ let block = null;
900
1080
  if (plainThinking) {
901
- parts.push(thinkingTemplate({ body: only.thinking, cls: "bubble thinking" }));
1081
+ block = thinkingTemplate({ body: only.thinking, cls: "bubble thinking" });
902
1082
  } else if (group.steps.length) {
903
1083
  // A step's text with its message's rendered markdown (html: web.markdown on).
904
1084
  const gens = group.steps.map((step) => ({
@@ -916,7 +1096,7 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
916
1096
  // Its summary line: the working set's footprint, when the turn touched
917
1097
  // something, then the steps (the live block's, stage_view.js handOff).
918
1098
  const foot = workingSet ? footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal) : nothing;
919
- parts.push(html`<details class="turn-work done"><summary><span class="tw-sum">${foot}<span class="turn-steps">${model.blockSummary({ gens, ended: true })}</span></span></summary>${steps}</details>`);
1099
+ block = html`<details class="turn-work done"><summary><span class="tw-sum">${foot}<span class="turn-steps">${model.blockSummary({ gens, ended: true })}</span></span></summary>${steps}</details>`;
920
1100
  outlines.push(gens);
921
1101
  // No record: a turn saved before them, or running at the reload.
922
1102
  blockEnds.push(group.record?.status ?? "completed");
@@ -925,7 +1105,6 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
925
1105
  // Steers with no step to sit in, or in a turn that shows no block: on
926
1106
  // their own after the prompt.
927
1107
  const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
928
- parts.push(loose.map((i) => steerTemplate(i, true)));
929
1108
  // The user's lines merged into the turn: the live "steered" bubbles,
930
1109
  // after its steps and before its answer. A delegate report's is
931
1110
  // followed by the text that answered it when that isn't the answer: a
@@ -934,31 +1113,37 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
934
1113
  // A report's fold key (answer_fold.js), as its reply's: the turn's id
935
1114
  // and its place after the opening; none in a turn saved before turn ids.
936
1115
  const lineKey = (i) => (group.turnId && items[i]?.source === "delegate_report" ? `${group.turnId}:input:${i - group.user}` : null);
937
- parts.push((group.merged || []).map((i) =>
938
- html`${userTemplate(items[i], { state: "steered", step: items[i].step, lineKey: lineKey(i) })}${reportReplyTemplate(i, group)}`));
939
- // A turn that ended with no answer: chi's muted notice where the answer
940
- // would be, before the timing, as live.
941
- if (group.emptyAnswer != null) parts.push(html`${emptyAnswerTemplate(items[group.emptyAnswer])}${timingTemplate(group)}`);
942
- // A turn with no answer (canceled, or running: no record yet) shows its
943
- // timing after its prompt and steps, where the live view leaves it.
944
- else if (group.answer === null) parts.push(timingTemplate(group));
945
- else parts.push(answerTemplate(group.answer, timingTemplate(group), group.turnId));
1116
+ const merged = (group.merged || []).map((i) =>
1117
+ html`${userTemplate(items[i], { state: "steered", step: items[i].step, lineKey: lineKey(i) })}${reportReplyTemplate(i, group)}`);
946
1118
  // A participant's review turn with a reply: Apply under it (the owner's
947
1119
  // page only: the participant page has its own view). Not under a pair
948
1120
  // turn: it acted itself.
949
1121
  const reviewer = group.answer !== null && prompt.turn_id && prompt.participant_mode !== PAIR
950
1122
  ? authorBadge(prompt.author, prompt.author_name) : null;
951
- if (reviewer) parts.push(applyRowTemplate(prompt.turn_id, reviewer));
952
- // A canceled turn ends with its cancel line, a failed one whose steps
953
- // stayed with its failure line, as the live view left them.
954
- parts.push(cancelLineTemplate(group.record));
955
- parts.push(failedLineTemplate(group.record));
1123
+ // The live hand-off's order (turn_pieces.js): a turn that ended with no
1124
+ // answer has chi's muted notice where the answer would be, before the
1125
+ // timing; a turn with no answer (canceled, or running: no record yet)
1126
+ // its timing after its prompt and steps. A canceled turn ends with its
1127
+ // cancel line, a failed one whose steps stayed with its failure line, as
1128
+ // the live view left them.
1129
+ parts.push(turnPieces({
1130
+ prompt: promptPiece,
1131
+ block,
1132
+ steers: loose.map((i) => steerTemplate(i, true)),
1133
+ merged,
1134
+ answer: group.emptyAnswer == null && group.answer !== null ? [answerBubble(group.answer, "", group.turnId)] : [],
1135
+ emptyNote: group.emptyAnswer != null ? emptyAnswerTemplate(items[group.emptyAnswer]) : null,
1136
+ timing: timingTemplate(group),
1137
+ apply: reviewer ? applyRowTemplate(prompt.turn_id, reviewer) : null,
1138
+ end: html`${cancelLineTemplate(group.record)}${failedLineTemplate(group.record)}`,
1139
+ }));
956
1140
  }
957
1141
  const out = nodesOf(parts);
958
1142
  // A finished turn's block as the live end leaves it: its outline, closed
959
1143
  // unless the turn was cut short.
960
1144
  const blocks = [...out.children].filter((el) => el.matches(".turn-work"));
961
- blocks.forEach((block, i) => { block.open = openAfterEnd(blockEnds[i], outlineBlock(block, outlines[i] || [])); });
1145
+ // A drawn block is outlined and opened as its live end left it.
1146
+ blocks.forEach((block, i) => { if (outlines[i] !== null) block.open = openAfterEnd(blockEnds[i], outlineBlock(block, outlines[i] || [])); });
962
1147
  if (workingSet) stampHistory(blocks, blockTurns);
963
1148
  // Each answer bubble and step text knows its message's refs
964
1149
  // (answer_refs.js; the page pairs them after the render), the index
@@ -0,0 +1,43 @@
1
+ // A finished turn's drawing (chi web's turn_drawings: {events, end}, the
2
+ // Bridge's TurnDrawings unfolded once, in Ruby): what the live page drew for
3
+ // the turn, which a reload draws again (turn_block.js drawnTurnNodes feeds
4
+ // these events to a live turn block) instead of rebuilding the turn from its
5
+ // saved messages. Pure, no DOM: timing.js folds a drawing into its steps
6
+ // here (the working set's rows, the footprint), and a new module, so a
7
+ // cached older page never imports a name its modules lack.
8
+ import * as model from "./turn_model.js";
9
+
10
+ const END_KINDS = Object.freeze({ turn_completed: "completed", turn_canceled: "canceled", turn_failed: "failed" });
11
+
12
+ // How the drawn turn ended ("completed", "canceled", "failed"), from its end
13
+ // event's type.
14
+ export function drawnKind(end) {
15
+ return END_KINDS[end?.type] || "completed";
16
+ }
17
+
18
+ // The drawing's events folded as turn_model.js folds a live turn (the same
19
+ // exports the turn block calls), ended: its gens, each with its tool rows.
20
+ export function drawingTurn(drawing) {
21
+ const turn = model.newTurn();
22
+ for (const event of drawing?.events || []) {
23
+ switch (event?.type) {
24
+ case "turn_started": model.turnStarted(turn); break;
25
+ case "generation_started": model.generationStarted(turn, event.iteration ?? null); break;
26
+ case "generation_chunk":
27
+ if (event.text || event.thinking) model.chunk(turn, { text: event.text || "", thinking: event.thinking || "", iteration: event.iteration ?? null });
28
+ break;
29
+ case "tool_call_started": model.toolStarted(turn, event); break;
30
+ case "tool_call_completed": model.toolCompleted(turn, event); break;
31
+ default: break;
32
+ }
33
+ }
34
+ model.turnEnded(turn);
35
+ return turn;
36
+ }
37
+
38
+ // The ids of the drawn turns among +items+ (their `drawn` stamps, a
39
+ // drawing for each in +drawings+).
40
+ export function drawnIds(items, drawings) {
41
+ if (!drawings) return new Set();
42
+ return new Set((items || []).map((item) => item?.drawn).filter((id) => typeof id === "string" && drawings[id]));
43
+ }
@@ -346,6 +346,10 @@ export function replaysDrawnTurn(event, turnRecords = []) {
346
346
  return turnRecords.some((record) => record.id === event.turn_id);
347
347
  }
348
348
 
349
+ // REMOVE one release after chi web sends replay (see
350
+ // ~/.claude/plans/arch-turn-records.md): chi web's show sends these events
351
+ // itself (`replay`, TurnAccumulator.join_events); kept exported meanwhile, as
352
+ // an old cached app.js imports it from this module.
349
353
  // A session view from the server (GET /api/sessions/:id on a live worker)
350
354
  // as the live events that would have drawn it: the turn in progress (its
351
355
  // parts in order), then the prompts queued behind it. app.js feeds these to
@@ -441,6 +445,10 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
441
445
  closeText();
442
446
  events.push({ type: "pending_input_merged", count: 0, content: null, steers: [{ source: part.source, text: part.text }] });
443
447
  break;
448
+ // A join draws the turn's cards from the snapshot's (CardStore), as
449
+ // TurnAccumulator.replay_events leaves them out.
450
+ case "card":
451
+ break;
444
452
  default:
445
453
  break;
446
454
  }
@@ -453,7 +461,8 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
453
461
  }
454
462
  // Commands waiting for the turn's end: their bubbles, as live.
455
463
  for (const entry of commands || []) {
456
- events.push({ type: "command_queued", command_id: entry.command_id, client_id: entry.client_id, line: entry.line, waits: "turn_end" });
464
+ const author = entry.author ? { author: entry.author } : {};
465
+ events.push({ type: "command_queued", command_id: entry.command_id, client_id: entry.client_id, line: entry.line, waits: "turn_end", ...author });
457
466
  }
458
467
  return events;
459
468
  }
@@ -0,0 +1,29 @@
1
+ // A finished turn's pieces in one order, and which step's text is its
2
+ // answer: the one rule the live hand-off (stage_model.js handOffOrder, the
3
+ // turn block's turnEnded) and a reload (turn_block.js turnHistoryNodes)
4
+ // both follow, so the two can't place a piece differently. Pure: the pieces
5
+ // are whatever the caller draws (nodes, lit templates); a missing one is
6
+ // skipped.
7
+
8
+ // The turn's pieces in order: its prompt (none for a reminder turn), its
9
+ // block of steps, the steers no step holds, the lines merged into it in
10
+ // arrival order (live: every extra, cards too), the answer's pieces or the
11
+ // notice of a turn with none, the timing line, a participant's review Apply,
12
+ // the end line (cancel, failure), the kept cards, then the lines queued for
13
+ // after the turn (they run next).
14
+ export function turnPieces({ prompt = null, block = null, steers = [], merged = [], answer = [], emptyNote = null, timing = null,
15
+ apply = null, end = null, kept = [], queued = [] } = {}) {
16
+ return [prompt, block, ...steers, ...merged, ...answer, emptyNote, timing, apply, end, ...kept, ...queued].filter(Boolean);
17
+ }
18
+
19
+ // The generation whose text is +turn+'s answer when it ended as +kind+
20
+ // ("completed", "canceled", "failed"), else null: its current (last, still
21
+ // open) generation, for a completed turn always, for any turn of one step
22
+ // without tools too (canceled, it is the partial text). +turn+: turn_model.js's
23
+ // {gens: [{tools, closed}]}.
24
+ export function answerGen(turn, kind) {
25
+ const gens = turn?.gens || [];
26
+ const last = gens[gens.length - 1];
27
+ if (!last || last.closed) return null;
28
+ return kind === "completed" || (gens.length === 1 && last.tools.length === 0) ? last : null;
29
+ }
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../log"
4
+ require_relative "../worker_link"
5
+
6
+ module Samagotchi
7
+ module Web
8
+ # POST /api/sessions/:id/reload_check, mixed into App: the opt-in reload
9
+ # check's report (web.reload_check; the page's reload_check.js compares a
10
+ # finished turn as it was drawn live with a reload of it, history_shape.js).
11
+ # One log line per check, counts only, never content: reload_checked,
12
+ # and reload_mismatch with the kinds that differed and the ACCEPTED
13
+ # entries that would excuse them (the page's check is strict). The pod count is
14
+ # `grep -c reload_mismatch` over `grep -c reload_checked`, split by
15
+ # kinds, drawn and worker_version. See docs/configuration.md.
16
+ module ReloadCheckRoute
17
+ # history_shape.js KINDS: the only tokens a kinds list may carry
18
+ # (spec/web/reload_check_route_spec.rb reads the JS list).
19
+ KINDS = %w[prompt steered note command recap block step steer row notice card question thinking answer
20
+ empty-answer timing cancel failed other].freeze
21
+ # history_shape.js ACCEPTED's ids: the only tokens an excused list may
22
+ # carry (the same spec reads the JS list).
23
+ ACCEPTED_IDS = %w[command-bubble edit-mark].freeze
24
+ # A turn id the line may name (a uuid, a test's id); anything else is
25
+ # dropped.
26
+ TURN_ID = /\A[0-9A-Za-z_-]{1,64}\z/
27
+
28
+ # The turns already checked, by session and turn id: every open owner
29
+ # tab runs the check, and only the first one's report is logged (a
30
+ # second tab's is answered 204 and dropped). Bounded: the oldest go
31
+ # first past LIMIT; a restarted chi web starts over.
32
+ class Seen
33
+ LIMIT = 2000
34
+
35
+ def initialize
36
+ @mutex = Mutex.new
37
+ @keys = {}
38
+ end
39
+
40
+ # Whether +key+ is new (and now seen).
41
+ def first?(key)
42
+ @mutex.synchronize do
43
+ next false if @keys.key?(key)
44
+
45
+ @keys[key] = true
46
+ @keys.shift while @keys.size > LIMIT
47
+ true
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # {turn_id, same, kinds, excused, explained, drawn} from the page: 204,
55
+ # whatever it said
56
+ # (fire-and-forget); 400 for a body that isn't a JSON object.
57
+ # worker_version is chi web's own view of the session's worker (its
58
+ # sidecar), never the body's.
59
+ def handle_reload_check(req, id)
60
+ body = parse_json(request_body(req, 4096))
61
+ return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
62
+
63
+ turn_id = body["turn_id"].to_s.match?(TURN_ID) ? body["turn_id"] : nil
64
+ # Another tab's report of a turn already counted.
65
+ return [204, { "Cache-Control" => "no-store" }, []] if turn_id && !@reload_checks.first?([id, turn_id])
66
+
67
+ same = body["same"] == true
68
+ drawn = body["drawn"] == true
69
+ fields = { sid: id, turn_id: turn_id, same: same, drawn: drawn, worker_version: checked_worker_version(id) }
70
+ Log.info(:web, "reload_checked", **fields)
71
+ unless same
72
+ Log.info(:web, "reload_mismatch", **fields.except(:same), kinds: checked_tokens(body["kinds"], KINDS),
73
+ excused: checked_tokens(body["excused"], ACCEPTED_IDS),
74
+ explained: body["explained"] == true)
75
+ end
76
+ [204, { "Cache-Control" => "no-store" }, []]
77
+ end
78
+
79
+ # +tokens+ as a sorted comma list of +known+'s, the rest dropped.
80
+ def checked_tokens(tokens, known)
81
+ Array(tokens).map(&:to_s).select { |token| known.include?(token) }.uniq.sort.join(",")
82
+ end
83
+
84
+ # The version of the chi the session's live worker runs (its
85
+ # sidecar), nil without one.
86
+ def checked_worker_version(id)
87
+ dir = @session_class.session_dir(id, state_dir: default_state_dir)
88
+ link = WorkerLink.resolve(dir, unlink: false)
89
+ link.live? ? link.sidecar&.version : nil
90
+ rescue StandardError
91
+ nil # degrade-ok: a line without the version is still counted
92
+ end
93
+ end
94
+ end
95
+ end
@@ -100,6 +100,7 @@ module Samagotchi
100
100
  annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
101
101
  editor: Config::BY_KEY["web.editor"].default, new_token: false,
102
102
  working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil, start_dir_from: :flag,
103
+ reload_check: Config::BY_KEY["web.reload_check"].default,
103
104
  restarted: false, restart_argv: ["web"], exec: nil)
104
105
  handed_on = false # a :restart leaves Serve to the next process, a run turns it off itself
105
106
  start_given = !start_dir.to_s.strip.empty?
@@ -159,7 +160,7 @@ module Samagotchi
159
160
  Push.preflight!(public_url)
160
161
  options = { port: port, host: host, url: url, open_browser: open_browser, markdown: markdown,
161
162
  annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set,
162
- public_url: public_url, start_dir: start_dir }
163
+ reload_check: reload_check, public_url: public_url, start_dir: start_dir }
163
164
  started = if plan
164
165
  with_tunnel(provider, plan) do |up|
165
166
  start(**options, public_notes: provider.start_lines(plan, restarted: restarted), on_bound: up)
@@ -388,6 +389,7 @@ module Samagotchi
388
389
  annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
389
390
  editor: Config::BY_KEY["web.editor"].default, token_path: Token.path,
390
391
  working_set: Config::BY_KEY["web.working_set"].default, public_url: nil, public_notes: [],
392
+ reload_check: Config::BY_KEY["web.reload_check"].default,
391
393
  on_bound: nil, start_dir: Dir.pwd, update_check: nil, auto_update: nil)
392
394
  config_restart_unsubscribe = ConfigFile.watch_restart_settings!(process: :web) do |message|
393
395
  Samagotchi::Log.warn(:config, "restart_required", echo: "Warning: config: #{message}")
@@ -404,7 +406,7 @@ module Samagotchi
404
406
  push = push_runtime(public_url, state_dir)
405
407
  app = App.new(manager: manager, state_dir: state_dir, markdown: markdown,
406
408
  annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option, working_set: working_set,
407
- public_url: public_url, public_token: public_token, push: push, start_dir: start_dir)
409
+ reload_check: reload_check, public_url: public_url, public_token: public_token, push: push, start_dir: start_dir)
408
410
  # update.auto: install; a stand-in app (a spec's) has no operations to run.
409
411
  auto_update ||= AutoUpdate.new(hub: hub, ops: app.update_ops, check: update_check, preflight: method(:restart_preflight)) if app.respond_to?(:update_ops)
410
412
  webrick = nil
@@ -109,8 +109,8 @@ module Samagotchi
109
109
  end
110
110
 
111
111
  # GET /api/sessions/summaries: the rows chi sessions list prints
112
- # (SessionManager.session_summaries: status, live, busy, owner, recap,
113
- # ctx_pct, waiting, …), filtered as its flags filter. Params: dir (a
112
+ # (SessionRow#to_summary_h, its 28 keys: status, live, busy, owner,
113
+ # recap, ctx_pct, waiting, …), filtered as its flags filter. Params: dir (a
114
114
  # folder: its project's sessions; none: every project's), cwd (in that
115
115
  # folder or below), live=1, archived=1, tests=0 (leave test runs out),
116
116
  # sort, order, limit. An array, newest first by default.
@@ -128,7 +128,7 @@ module Samagotchi
128
128
  project_root: dir && ProjectScope.root_for(dir), include_archived: truthy_param?(req.params["archived"]),
129
129
  sort: sanitize_sort(req.params["sort"]), order: sanitize_order(req.params["order"])
130
130
  )
131
- json_response(200, rows)
131
+ json_response(200, rows.map(&:to_summary_h))
132
132
  end
133
133
 
134
134
  # POST /api/sessions/:id/wait/prepare: what chi send --wait and chi
@@ -207,7 +207,7 @@ module Samagotchi
207
207
  when :accepted
208
208
  record_history(body, prompt)
209
209
  # via: bridge or file (the worker was gone: its input file), which
210
- # names the turn a remote chi send --wait follows (WaitTarget).
210
+ # names the turn a remote chi send --wait follows (ReplyWait::Target).
211
211
  json_response(202, result.ack.merge("via" => result.via.to_s))
212
212
  when :refused then json_response(result.code, result.ack)
213
213
  when :timeout then error_response(504, "worker_timeout", result.detail)