samagotchi 0.153.0 → 0.154.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/README.md +2 -3
  4. data/bin/chi +23 -24
  5. data/docs/architecture.md +30 -33
  6. data/docs/broadcast.md +5 -5
  7. data/docs/bundles.md +9 -12
  8. data/docs/cli.md +80 -96
  9. data/docs/configuration.md +27 -26
  10. data/docs/context.md +3 -3
  11. data/docs/desktop.md +1 -1
  12. data/docs/guardrails.md +8 -10
  13. data/docs/hooks.md +9 -9
  14. data/docs/internals/attached-context.md +2 -2
  15. data/docs/internals/background-tasks.md +3 -4
  16. data/docs/internals/block-renderers.md +4 -0
  17. data/docs/internals/bridge.md +3 -3
  18. data/docs/internals/context-telemetry.md +1 -1
  19. data/docs/internals/engine.md +33 -7
  20. data/docs/internals/objectives.md +6 -5
  21. data/docs/internals/participants.md +7 -7
  22. data/docs/internals/prompt-caching.md +1 -1
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-push.md +1 -1
  25. data/docs/memory.md +2 -2
  26. data/docs/plugins.md +23 -28
  27. data/docs/sessions.md +24 -24
  28. data/docs/sub-agent.md +2 -2
  29. data/docs/system-one.md +1 -1
  30. data/docs/testing.md +16 -6
  31. data/lib/samagotchi/answer_text.rb +34 -0
  32. data/lib/samagotchi/bridge.rb +43 -32
  33. data/lib/samagotchi/bridge_client.rb +10 -9
  34. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  35. data/lib/samagotchi/broadcast/triage.rb +1 -1
  36. data/lib/samagotchi/broadcast_command.rb +5 -5
  37. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  38. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  39. data/lib/samagotchi/bundle_command.rb +2 -2
  40. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  41. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  42. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  44. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  45. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  46. data/lib/samagotchi/child_move.rb +3 -3
  47. data/lib/samagotchi/commands/registry.rb +6 -6
  48. data/lib/samagotchi/composer_route.rb +4 -5
  49. data/lib/samagotchi/config.rb +14 -12
  50. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  51. data/lib/samagotchi/context_candidates.rb +4 -3
  52. data/lib/samagotchi/continue_offer.rb +1 -1
  53. data/lib/samagotchi/debug_log.rb +1 -1
  54. data/lib/samagotchi/degrade.rb +75 -0
  55. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  56. data/lib/samagotchi/engine/cards.rb +87 -0
  57. data/lib/samagotchi/engine/context_judge.rb +138 -0
  58. data/lib/samagotchi/engine/events.rb +131 -0
  59. data/lib/samagotchi/engine/guardrails.rb +87 -0
  60. data/lib/samagotchi/engine/hooks.rb +119 -0
  61. data/lib/samagotchi/engine/messages.rb +124 -0
  62. data/lib/samagotchi/engine/model_settings.rb +127 -0
  63. data/lib/samagotchi/engine/models.rb +261 -0
  64. data/lib/samagotchi/engine/objectives.rb +116 -0
  65. data/lib/samagotchi/engine/participants.rb +86 -0
  66. data/lib/samagotchi/engine/plugins.rb +208 -0
  67. data/lib/samagotchi/engine/questions.rb +98 -0
  68. data/lib/samagotchi/engine/recap.rb +130 -0
  69. data/lib/samagotchi/engine/session.rb +131 -0
  70. data/lib/samagotchi/engine/side_requests.rb +135 -0
  71. data/lib/samagotchi/engine/steering.rb +134 -0
  72. data/lib/samagotchi/engine/toolkit.rb +269 -0
  73. data/lib/samagotchi/engine/turn_end.rb +180 -0
  74. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  75. data/lib/samagotchi/engine/verify.rb +153 -0
  76. data/lib/samagotchi/engine.rb +62 -2646
  77. data/lib/samagotchi/grants.rb +575 -0
  78. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  79. data/lib/samagotchi/guardrails/context.rb +2 -3
  80. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  81. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  82. data/lib/samagotchi/hook_routes.rb +3 -3
  83. data/lib/samagotchi/idle_recap.rb +4 -5
  84. data/lib/samagotchi/idle_reminders.rb +3 -4
  85. data/lib/samagotchi/iteration_limit.rb +2 -2
  86. data/lib/samagotchi/kernel_loop.rb +10 -11
  87. data/lib/samagotchi/launch_mode.rb +13 -32
  88. data/lib/samagotchi/level_setting_command.rb +3 -2
  89. data/lib/samagotchi/llm_context_command.rb +3 -2
  90. data/lib/samagotchi/log.rb +7 -7
  91. data/lib/samagotchi/log_line.rb +1 -1
  92. data/lib/samagotchi/log_path.rb +1 -1
  93. data/lib/samagotchi/note_command.rb +1 -1
  94. data/lib/samagotchi/note_delivery.rb +18 -8
  95. data/lib/samagotchi/objectives.rb +24 -3
  96. data/lib/samagotchi/objectives_check.rb +8 -27
  97. data/lib/samagotchi/objectives_command.rb +4 -69
  98. data/lib/samagotchi/objectives_edit.rb +87 -0
  99. data/lib/samagotchi/output_formatter.rb +7 -1
  100. data/lib/samagotchi/owner_lock.rb +7 -1
  101. data/lib/samagotchi/parent_answers_command.rb +5 -4
  102. data/lib/samagotchi/participant_exposure.rb +42 -0
  103. data/lib/samagotchi/participant_mode.rb +1 -1
  104. data/lib/samagotchi/participant_name.rb +1 -1
  105. data/lib/samagotchi/participants.rb +17 -17
  106. data/lib/samagotchi/participants_watch.rb +1 -1
  107. data/lib/samagotchi/pending_input_queue.rb +2 -2
  108. data/lib/samagotchi/plugin/api.rb +3 -2
  109. data/lib/samagotchi/plugin/context.rb +21 -7
  110. data/lib/samagotchi/plugin/peer.rb +2 -1
  111. data/lib/samagotchi/plugin/sessions.rb +6 -4
  112. data/lib/samagotchi/plugin_tasks.rb +1 -2
  113. data/lib/samagotchi/private_files.rb +29 -0
  114. data/lib/samagotchi/project_env.rb +2 -2
  115. data/lib/samagotchi/question_desk.rb +8 -7
  116. data/lib/samagotchi/recap_command.rb +2 -1
  117. data/lib/samagotchi/recap_setup.rb +1 -1
  118. data/lib/samagotchi/recap_store.rb +1 -1
  119. data/lib/samagotchi/redact.rb +86 -0
  120. data/lib/samagotchi/reminder_queue.rb +2 -2
  121. data/lib/samagotchi/remote_attach_command.rb +1 -1
  122. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  123. data/lib/samagotchi/review_start.rb +1 -1
  124. data/lib/samagotchi/run_options.rb +6 -6
  125. data/lib/samagotchi/self_report.rb +42 -39
  126. data/lib/samagotchi/send_command/busy.rb +1 -1
  127. data/lib/samagotchi/send_command.rb +4 -1
  128. data/lib/samagotchi/session.rb +11 -2
  129. data/lib/samagotchi/session_access/contract.rb +3 -3
  130. data/lib/samagotchi/session_access/local.rb +9 -9
  131. data/lib/samagotchi/session_access/remote.rb +17 -7
  132. data/lib/samagotchi/session_access.rb +4 -4
  133. data/lib/samagotchi/session_archive_command.rb +2 -2
  134. data/lib/samagotchi/session_chain.rb +8 -6
  135. data/lib/samagotchi/session_commands.rb +45 -48
  136. data/lib/samagotchi/session_continue.rb +1 -1
  137. data/lib/samagotchi/session_delete_command.rb +4 -4
  138. data/lib/samagotchi/session_manager.rb +59 -36
  139. data/lib/samagotchi/session_metrics.rb +2 -2
  140. data/lib/samagotchi/session_model_settings.rb +3 -2
  141. data/lib/samagotchi/session_participants.rb +1 -1
  142. data/lib/samagotchi/session_retention.rb +3 -3
  143. data/lib/samagotchi/sessions_command.rb +4 -4
  144. data/lib/samagotchi/share_command.rb +10 -10
  145. data/lib/samagotchi/steer.rb +1 -1
  146. data/lib/samagotchi/system_prompt.rb +102 -109
  147. data/lib/samagotchi/task_watch.rb +1 -1
  148. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  149. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  150. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  151. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  152. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  153. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  154. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  155. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  156. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  157. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  158. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  159. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  160. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  161. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  163. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  164. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  165. data/lib/samagotchi/terminal_ui.rb +142 -1387
  166. data/lib/samagotchi/title_command.rb +2 -2
  167. data/lib/samagotchi/tool_runner.rb +14 -16
  168. data/lib/samagotchi/toolkit_command.rb +2 -1
  169. data/lib/samagotchi/tools/delegate.rb +3 -1
  170. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  171. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  172. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  173. data/lib/samagotchi/tools/send_note.rb +2 -1
  174. data/lib/samagotchi/turn_endings.rb +2 -2
  175. data/lib/samagotchi/turn_flow.rb +4 -5
  176. data/lib/samagotchi/update_hint.rb +2 -2
  177. data/lib/samagotchi/version.rb +1 -1
  178. data/lib/samagotchi/web/app.rb +39 -50
  179. data/lib/samagotchi/web/participant_access.rb +2 -2
  180. data/lib/samagotchi/web/participant_review.rb +5 -5
  181. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  182. data/lib/samagotchi/web/participant_view.rb +2 -82
  183. data/lib/samagotchi/web/participants_admin.rb +1 -1
  184. data/lib/samagotchi/web/public/block_host.js +35 -8
  185. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  186. data/lib/samagotchi/web/public/chains.js +2 -2
  187. data/lib/samagotchi/web/public/claim_link.js +4 -4
  188. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  189. data/lib/samagotchi/web/public/format.js +3 -3
  190. data/lib/samagotchi/web/push/store.rb +1 -1
  191. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  192. data/lib/samagotchi/web/session_control.rb +12 -8
  193. data/lib/samagotchi/web/session_create.rb +1 -1
  194. data/lib/samagotchi/web/session_hub.rb +2 -2
  195. data/lib/samagotchi/web/session_show.rb +2 -2
  196. data/lib/samagotchi/web/session_summary.rb +1 -1
  197. data/lib/samagotchi/worker.rb +100 -9
  198. data/lib/samagotchi/worker_link.rb +3 -3
  199. data/lib/samagotchi/working_set_command.rb +3 -2
  200. metadata +29 -6
  201. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  202. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  203. data/lib/samagotchi/web/grants.rb +0 -575
  204. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  205. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -42,6 +42,11 @@ const NOT_AN_IMAGE = "the renderer's SVG can't be shown as an image";
42
42
  // this tall and scrolls sideways instead.
43
43
  const MIN_SHOWN_HEIGHT = 120;
44
44
 
45
+ // The zoom button's words: the picture fitted to the column, or at its natural size.
46
+ const ZOOM_IN = "Show at full size";
47
+ const ZOOM_OUT = "Fit to the column";
48
+
49
+ const ZOOM_ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><circle cx="7" cy="7" r="4.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m10.2 10.2 3.3 3.3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
45
50
  const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 4 2 8l3.5 4M10.5 4 14 8l-3.5 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
46
51
 
47
52
  // The picture's URL and size. The XML parser reads the root, as the image will: a
@@ -286,6 +291,9 @@ export function createBlockHost({
286
291
  return Promise.race([image, bound]).finally(() => clearTimeout(timer));
287
292
  }
288
293
 
294
+ // The picture's name says where its words are: the source <pre> is hidden, so a
295
+ // screen reader reaches it through the toggle. The zoom is a button too, for the
296
+ // keyboard (a click on the picture does the same); it shows on keyboard focus only.
289
297
  function makeFigure(picture, themeName, lang) {
290
298
  const figure = doc.createElement("figure");
291
299
  figure.className = DRAWN;
@@ -294,17 +302,33 @@ export function createBlockHost({
294
302
  scroll.className = "block-scroll";
295
303
  const img = doc.createElement("img");
296
304
  img.className = "block-img";
297
- img.alt = `${lang} diagram`;
305
+ img.alt = `${lang} diagram, its source under the Show the source button`;
298
306
  showPicture(img, picture);
299
307
  scroll.append(img);
300
- const toggle = doc.createElement("button");
301
- toggle.type = "button";
302
- toggle.className = "block-toggle";
303
- litRender(ICON, toggle);
304
- figure.append(scroll, toggle);
308
+ const zoom = button("block-zoom", ZOOM_ICON);
309
+ const toggle = button("block-toggle", ICON);
310
+ figure.append(scroll, zoom, toggle);
311
+ setZoomed(figure, false);
305
312
  return figure;
306
313
  }
307
314
 
315
+ function button(className, icon) {
316
+ const el = doc.createElement("button");
317
+ el.type = "button";
318
+ el.className = className;
319
+ litRender(icon, el);
320
+ return el;
321
+ }
322
+
323
+ function setZoomed(figure, on) {
324
+ figure.classList.toggle("natural", on);
325
+ const zoom = figure.querySelector(":scope > .block-zoom");
326
+ if (!zoom) return;
327
+ zoom.setAttribute("aria-pressed", on ? "true" : "false");
328
+ zoom.setAttribute("aria-label", on ? ZOOM_OUT : ZOOM_IN);
329
+ zoom.title = on ? ZOOM_OUT : ZOOM_IN;
330
+ }
331
+
308
332
  function showPicture(img, { url, width, height }) {
309
333
  if (width && height) {
310
334
  img.setAttribute("width", width);
@@ -476,7 +500,7 @@ export function createBlockHost({
476
500
  scan(root);
477
501
 
478
502
  root.addEventListener("mousedown", (e) => {
479
- if (e.target.closest?.(".block-toggle")) e.preventDefault();
503
+ if (e.target.closest?.(".block-toggle, .block-zoom")) e.preventDefault();
480
504
  });
481
505
  // Delegated, so a Revise clone's work too.
482
506
  root.addEventListener("click", (e) => {
@@ -490,7 +514,10 @@ export function createBlockHost({
490
514
  }
491
515
  const note = e.target.closest?.(".block-note");
492
516
  if (note) { copy(note.dataset.text); return; }
493
- if (e.target.closest?.(".block-img")) e.target.closest(`.${DRAWN}`).classList.toggle("natural");
517
+ if (e.target.closest?.(".block-zoom, .block-img")) {
518
+ const figure = e.target.closest(`.${DRAWN}`);
519
+ setZoomed(figure, !figure.classList.contains("natural"));
520
+ }
494
521
  });
495
522
  }
496
523
 
@@ -1,4 +1,4 @@
1
- /* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. */
1
+ /* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. The zoom button, the keyboard's way to the natural size, shows on keyboard focus only, left of the toggle. */
2
2
  .block-drawn{position:relative;margin:0 0 12px;white-space:normal}
3
3
  .code-wrap>.block-drawn{margin:0}
4
4
  .code-wrap>.block-drawn.source{height:0}
@@ -8,11 +8,15 @@
8
8
  .block-img{display:block;max-width:100%;max-height:50000px;object-fit:contain;height:auto;margin:0 auto;cursor:zoom-in}
9
9
  .block-drawn.natural .block-img{max-width:50000px;margin:0;cursor:zoom-out}
10
10
  .block-drawn.source .block-scroll{display:none}
11
- .block-toggle{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
11
+ .block-toggle,.block-zoom{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
12
12
  .code-wrap>.block-drawn>.block-toggle{right:34px}
13
- .block-toggle svg{display:block}
14
- .block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
15
- .block-toggle:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled){color:var(--text-bright)}}
13
+ .block-zoom{right:34px}
14
+ .code-wrap>.block-drawn>.block-zoom{right:63px}
15
+ .block-drawn.source>.block-zoom{display:none}
16
+ .block-zoom:not(:focus-visible){pointer-events:none}
17
+ .block-toggle svg,.block-zoom svg{display:block}
18
+ .block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-zoom:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
19
+ .block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){color:var(--text-bright)}}
16
20
  .block-drawn.source:not(:last-child){margin:0}
17
21
  /* Its words are the attribute's, so a bubble's text stays the answer's (block_host.js fail). */
18
22
  .block-note{margin:4px 0 12px;font-size:12px;color:var(--text-3);white-space:normal}
@@ -305,8 +305,8 @@ export function continueRefusal(error, sessions = []) {
305
305
  }
306
306
 
307
307
  // The open-children refusal as the toast shows it: the open delegates the
308
- // continue can't move to the next link (an older chi's worker, a chi
309
- // REPL), each a link to its session with what to do about it.
308
+ // continue can't move to the next link (an older chi's worker, a
309
+ // one-shot run), each a link to its session with what to do about it.
310
310
  // @param hrefFor (id) → its page's href
311
311
  export function openChildrenTemplate(prevId, children, hrefFor) {
312
312
  const n = children.length;
@@ -12,7 +12,7 @@
12
12
  // URL-safe and a space is one "+".
13
13
  //
14
14
  // Normalized: whitespace collapsed, and what the participant page shows
15
- // instead (ParticipantView::Redact): secret shapes "[redacted]" (so a link
15
+ // instead (Samagotchi::Redact): secret shapes "[redacted]" (so a link
16
16
  // never carries one, and matches the participant's redacted text), and on
17
17
  // the owner's page the session's folder relative ("<cwd>/x" → "x",
18
18
  // "<cwd>" → "."), the home folder "~". Not the owner's token (Redact's
@@ -41,10 +41,10 @@ export const HIT_CLASS = "claim-hit";
41
41
  const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
42
 
43
43
  export const MARK = "[redacted]";
44
- // ParticipantView::Redact's PATTERNS and VALUES
45
- // (lib/samagotchi/web/participant_view.rb), as JS: keep them in step.
44
+ // Samagotchi::Redact's PATTERNS and VALUES
45
+ // (lib/samagotchi/redact.rb), as JS: keep them in step.
46
46
  // spec/fixtures/redact_cases.json holds cases both sides read the same
47
- // (participant_view_spec.rb, claim_link.test.js).
47
+ // (redact_spec.rb, claim_link.test.js).
48
48
  const SECRET_PATTERNS = [
49
49
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
50
50
  /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
@@ -51,7 +51,7 @@ export function composerMode({ selected, owner, running, narrow }) {
51
51
  if (tui) {
52
52
  return {
53
53
  ...base,
54
- placeholder: pick("A terminal chi owns this session · run it with chi --shared to send from here", "A terminal chi owns this session"),
54
+ placeholder: pick("A terminal chi owns this session · its one-turn run (chi --non-interactive) lets go as it ends", "A terminal chi owns this session"),
55
55
  hint: "", button: "Send", disabled: true,
56
56
  };
57
57
  }
@@ -97,13 +97,13 @@ export function deleteConfirmText(session) {
97
97
  }
98
98
 
99
99
  // The badge on a session a process holds right now (the list's `owner`):
100
- // a worker (shared: every UI can send), or a plain terminal chi (not shared).
100
+ // a worker (every UI can send), or a terminal chi's one-turn run (--non-interactive).
101
101
  export function ownerBadge(owner, id) {
102
102
  if (owner === "worker") {
103
103
  return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
104
104
  }
105
105
  if (owner === "tui") {
106
- return { kind: "tui", text: "terminal", title: "A terminal chi owns this session and doesn't share it (chi --shared would)" };
106
+ return { kind: "tui", text: "terminal", title: "A terminal chi runs one turn in this session (chi --non-interactive) and lets go as it ends" };
107
107
  }
108
108
  return null;
109
109
  }
@@ -171,7 +171,7 @@ export function childrenSummary(parent, sessions) {
171
171
 
172
172
  // Whether the info bar offers "stop": only while a worker runs the session
173
173
  // (the list says so, or this page streams from one a send just woke). A
174
- // stopped or idle-exited session has nothing to stop, and a chi REPL's is
174
+ // stopped or idle-exited session has nothing to stop, and a one-turn run's is
175
175
  // refused by the server.
176
176
  export function canStopSession({ owner, streaming }) {
177
177
  if (owner === "tui") return false;
@@ -7,7 +7,7 @@ require "openssl"
7
7
  require "time"
8
8
  require "uri"
9
9
  require_relative "crypto"
10
- require_relative "../private_files"
10
+ require_relative "../../private_files"
11
11
  require_relative "../../log"
12
12
  require_relative "../../atomic_file"
13
13
  require_relative "../../session"
@@ -256,7 +256,7 @@ module Samagotchi
256
256
  # status row), parent_strategy, installed (the newest chi here)}. A
257
257
  # worker still leaving is waited for first (Local#wake: its recap's
258
258
  # timeout and a margin, on top of +wait+); 409
259
- # owned_by_tui (a chi REPL has it), 409 scratch (a leftover scratch
259
+ # owned_by_tui (a one-turn chi run has it), 409 scratch (a leftover scratch
260
260
  # session, as chi --attach here says); 504 worker_timeout (its Bridge
261
261
  # didn't come in time); 409 worker_newer (a worker whose protocol is newer
262
262
  # than this chi's, not resumed); 404 not_found.
@@ -425,7 +425,7 @@ module Samagotchi
425
425
  # session (NoteDelivery, as chi note; source: where it came from,
426
426
  # "cli" by default, never "broadcast"). 200 {status: "queued" (a
427
427
  # worker adds it) or "waits" (the next start does), session_id,
428
- # queued}; 409 owned_by_tui (status refused: a chi REPL has it); 400
428
+ # queued}; 409 owned_by_tui (status refused: a one-turn chi run has it); 400
429
429
  # note_rejected (empty, or over 16 KiB).
430
430
  def handle_notes(req, id)
431
431
  body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
@@ -438,8 +438,9 @@ module Samagotchi
438
438
  session_access.show(id)
439
439
  result = session_access.note(id, text: text, source: source.strip)
440
440
  if result.status == :refused
441
- error = SessionManager::OwnedByTUI.new(id)
442
- return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0)
441
+ error = result.scratch ? SessionManager::ScratchSession.new(id) : SessionManager::OwnedByTUI.new(id)
442
+ return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0,
443
+ scratch: result.scratch || nil)
443
444
  end
444
445
 
445
446
  json_response(200, { status: result.status.to_s, session_id: id, queued: result.queued })
@@ -16,7 +16,7 @@ module Samagotchi
16
16
  # a task, dequeue a queued turn, what it could stop and stop it
17
17
  # (handle_session_holds, handle_stop_holds — the page's Stop asks about
18
18
  # these first), stop, restart, archive, unarchive and delete
19
- # (handle_question_answer … handle_delete), with the refusals they answer (owned_by_repl, owned_by_tui, archive_refused). Included in App:
19
+ # (handle_question_answer … handle_delete), with the refusals they answer (owned_by_one_shot, owned_by_tui, archive_refused). Included in App:
20
20
  # its ROUTES name the handlers, and its helpers (json_response,
21
21
  # error_response, parse_json, request_body, relay, not_live,
22
22
  # bridge_error, reply_detail, worker_timeout, bridge_client,
@@ -443,7 +443,7 @@ module Samagotchi
443
443
 
444
444
  # POST /api/sessions/:id/archive: hide it (and its delegates) from the
445
445
  # lists, keep it for good. A live idle worker is stopped first; a turn
446
- # running in it or a delegate, a chi REPL's session and a scratch one
446
+ # running in it or a delegate, a one-turn chi run's session and a scratch one
447
447
  # are refused (409).
448
448
  def handle_archive(_req, id)
449
449
  result = @manager.archive_session(id, state_dir: @state_dir, wait: STOP_WAIT_SECONDS)
@@ -451,7 +451,7 @@ module Samagotchi
451
451
  json_response(200, { status: "archived", session_id: result.id, archived: result.archived,
452
452
  stopped: result.stopped, discarded: result.discarded })
453
453
  rescue SessionManager::OwnedByTUI => e
454
- owned_by_repl(id, e)
454
+ owned_by_one_shot(id, e)
455
455
  rescue SessionManager::ArchiveRefused => e
456
456
  archive_refused(e)
457
457
  end
@@ -466,7 +466,7 @@ module Samagotchi
466
466
  # DELETE /api/sessions/:id?force=true: the session for good. With
467
467
  # force a live worker is stopped first (the page sends it: its confirm
468
468
  # says so); without it a live worker's session is refused, 409
469
- # worker_running, as chi sessions delete without --force. A chi REPL's
469
+ # worker_running, as chi sessions delete without --force. A one-turn chi run's
470
470
  # session is always refused.
471
471
  def handle_delete(req, id)
472
472
  result = @manager.delete_session(id, state_dir: @state_dir, stop: truthy_param?(req.params["force"]),
@@ -474,7 +474,7 @@ module Samagotchi
474
474
  @hub&.touch(result[:id])
475
475
  json_response(200, { status: "deleted", session_id: result[:id], stopped: result[:stopped] })
476
476
  rescue SessionManager::OwnedByTUI => e
477
- owned_by_repl(id, e)
477
+ owned_by_one_shot(id, e)
478
478
  rescue SessionManager::DeleteRefused => e
479
479
  detail = e.reason == :still_stopping ? "#{e.message}; try again in a moment" : e.message
480
480
  refusal_response(409, e.reason.to_s, e, detail: detail, session_id: e.session_id, reason: e.reason.to_s)
@@ -482,15 +482,19 @@ module Samagotchi
482
482
 
483
483
  # 409 owned_by_tui in the words a refused archive, delete or continue
484
484
  # shows (stop, restart, a turn and a command show OwnedByTUI's own).
485
- def owned_by_repl(id, error = nil)
485
+ def owned_by_one_shot(id, error = nil)
486
486
  named = SessionManager::OwnedByTUI.new(id)
487
487
  error ||= named
488
488
  refusal_response(409, "owned_by_tui", error, detail: named.message, session_id: error.session_id)
489
489
  end
490
490
 
491
491
  # 409 owned_by_tui in OwnedByTUI's own words (stop, restart, a turn, a
492
- # command).
493
- def owned_by_tui(error) = refusal_response(409, "owned_by_tui", error, session_id: error.session_id)
492
+ # command); scratch: true for a chi scratch session
493
+ # (SessionManager::ScratchSession), which a remote client raises again.
494
+ def owned_by_tui(error)
495
+ scratch = error.is_a?(SessionManager::ScratchSession) || nil
496
+ refusal_response(409, "owned_by_tui", error, session_id: error.session_id, scratch: scratch)
497
+ end
494
498
 
495
499
  # 409 scratch or busy (every other reason: a turn, a queued prompt, an
496
500
  # open step-limit question, in it or a delegate, +busy_id+), with the
@@ -339,7 +339,7 @@ module Samagotchi
339
339
  rescue SessionManager::ArchiveRefused => e
340
340
  [nil, archive_refused(e)]
341
341
  rescue SessionManager::OwnedByTUI => e
342
- [nil, owned_by_repl(given, e)]
342
+ [nil, owned_by_one_shot(given, e)]
343
343
  rescue ModelProfile::MissingModel => e
344
344
  [nil, error_response(400, "invalid_model", e.message)]
345
345
  rescue Session::AmbiguousId => e
@@ -41,8 +41,8 @@ module Samagotchi
41
41
  # The flock leaves no file trace when its holder dies, so owners are
42
42
  # probed: every tick for the sessions the projection believes owned
43
43
  # (a handful; OwnerLock.owner is open + LOCK_SH|LOCK_NB + close), and
44
- # all sessions this often (a plain REPL that opened an old session
45
- # without saving it has no file signal until its first save).
44
+ # all sessions this often (a one-shot or an older chi's REPL that
45
+ # opened an old session without saving it has no file signal until its first save).
46
46
  FULL_PROBE_INTERVAL = 10.0
47
47
 
48
48
  # What a subscriber gets: `session` with {session: summary} for an
@@ -120,8 +120,8 @@ module Samagotchi
120
120
  end
121
121
 
122
122
  # The file's question only while a worker owns the session: one a
123
- # dead worker saved can't be answered (its turn is gone), nor a chi
124
- # REPL's (it shares nothing with the web).
123
+ # dead worker saved can't be answered (its turn is gone), nor a
124
+ # one-shot's or an older chi's REPL's (they share nothing with the web).
125
125
  # A question between turns (the step-limit one) is the snapshot's own.
126
126
  # Its refs (the asking reply's, QuestionAsked) are checked again.
127
127
  def shown_pending_question(session, turn_snapshot, owner)
@@ -108,7 +108,7 @@ module Samagotchi
108
108
  # For the tab's notifications (notify.js): an open question, the
109
109
  # running turn's open card with actions, and how the last turn
110
110
  # ended. A question saved by a worker that died is not open, nor a
111
- # chi REPL's (only a worker shares it).
111
+ # one-turn chi run's (only a worker shares it).
112
112
  pending_question: session.waiting_question(live: !!owner&.worker?),
113
113
  pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
114
114
  last_turn: session.last_turn&.to_file,
@@ -14,6 +14,7 @@ require_relative "turn_note"
14
14
  require_relative "steer"
15
15
  require_relative "turn_origin"
16
16
  require_relative "worker_idle_exit"
17
+ require_relative "degrade"
17
18
  require_relative "hold_stop"
18
19
  require_relative "children_status"
19
20
  require_relative "worker_wakes"
@@ -51,7 +52,7 @@ module Samagotchi
51
52
  # It runs the session's Engine and Bridge, takes queued prompts from the
52
53
  # input dir one turn at a time, and returns when nobody has used it for the
53
54
  # idle-exit timeout. A failed turn is rolled back and its prompt handed back
54
- # (:prompt_restored), as in the REPL, and the loop goes on. A stop marked on
55
+ # (:prompt_restored), and the loop goes on. A stop marked on
55
56
  # disk exits the process; an error outside a turn marks the session and
56
57
  # exits with 1.
57
58
  #
@@ -188,6 +189,10 @@ module Samagotchi
188
189
  # checks again then).
189
190
  def discard? = @discard
190
191
 
192
+ # Whether this worker runs a `chi scratch` session: deleted as it leaves
193
+ # (SessionManager.run_session_loop).
194
+ def scratch? = !!@session&.scratch
195
+
191
196
  # Whether delegate reports wait that an idle parent runs a turn for
192
197
  # (#wake_state): SessionManager.run_session_loop starts a new worker
193
198
  # for them after this one leaves. Reports the budget or a failure held
@@ -201,8 +206,9 @@ module Samagotchi
201
206
  # to exit (Bridge POST /exit), :restart when it asked for a restart
202
207
  # (the caller starts a new worker), :stopped when the session was stopped
203
208
  # (chi stop), :signal when a signal stopped it (WorkerSignals),
204
- # :crashed when the loop raised (the session is marked errored);
205
- # SessionManager.run_session_loop turns it into the exit
209
+ # :crashed when the loop raised (the session is marked errored),
210
+ # :scratch_left when a scratch session's terminal is gone (the caller
211
+ # deletes it); SessionManager.run_session_loop turns it into the exit
206
212
  def run
207
213
  # The thread a signal's expired grace raises in (the main thread in a
208
214
  # worker process).
@@ -248,6 +254,8 @@ module Samagotchi
248
254
  # The session, its Engine and what runs its turns, before anyone can
249
255
  # reach it.
250
256
  def start
257
+ # A scratch worker's grace for its terminal's first stream counts from here.
258
+ @started_at = monotonic_now
251
259
  # The wake turns' budget (delegate reports, attached context, peers), from now.
252
260
  @wakes = WorkerWakes.new(grace: WAKE_START_GRACE)
253
261
  # This worker holds the session now (a continue's starting marker held
@@ -340,7 +348,7 @@ module Samagotchi
340
348
  @bridge.queue_command(line, client_id: client_id, after_file: after_file,
341
349
  enqueued_id: enqueued_id, author: author)
342
350
  })
343
- # /model's default is the config's, as in the REPL; the Engine started
351
+ # /model's default is the config's; the Engine started
344
352
  # on the session's model. nil when there is no default.model.
345
353
  @default_model = ModelProfile.default_model_name
346
354
  @commands = SessionCommands.new(engine: @engine, turn_flow: @turn_flow,
@@ -416,6 +424,31 @@ module Samagotchi
416
424
  @context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
417
425
  project_root: @session.project_root, cwd: @session.working_directory,
418
426
  on_change: -> { @waker.wake }).start
427
+ @scratch_watch = start_scratch_watch if @session.scratch
428
+ end
429
+
430
+ # A scratch session's turn never runs on with no terminal: a turn
431
+ # waiting on a question or an approval would keep the worker up for
432
+ # nobody. Each tick while a turn runs and its terminal is gone
433
+ # (#scratch_unattended?), the question is dropped and the turn
434
+ # cancelled as a stop cancels it; the idle pass then leaves
435
+ # (:scratch_left) and the session is deleted.
436
+ def start_scratch_watch
437
+ Thread.new do
438
+ loop do
439
+ sleep(@poll_interval)
440
+ Degrade.call(:worker, "scratch_watch_failed") { cancel_unattended_turn }
441
+ end
442
+ end.tap { |thread| thread.report_on_exception = false }
443
+ end
444
+
445
+ def cancel_unattended_turn
446
+ return unless @engine.turn_running? && scratch_unattended?
447
+
448
+ Log.info(:worker, "scratch_turn_cancelled", sid: @session_id)
449
+ @engine.cancel_question(SessionManager::STOP_CANCEL_REASON) if @engine.pending_question
450
+ @engine.cancel_current_turn!(SessionManager::STOP_CANCEL_REASON.to_sym)
451
+ @waker.wake
419
452
  end
420
453
 
421
454
  # One pass of the loop: the first work found runs (queued commands, a
@@ -468,6 +501,7 @@ module Samagotchi
468
501
  return if run_wake_turn
469
502
  return if run_task_wake
470
503
  return if run_due_reminders
504
+ return left(:scratch_left) if scratch_unattended? && !@engine.turn_running? && leave_scratch
471
505
  return left(:idle_exit) if @idle_exit.due? && leave_idle
472
506
 
473
507
  leaving = @exit_requested && leave_on_request
@@ -553,6 +587,7 @@ module Samagotchi
553
587
  TEARDOWN_LEAVE_WAIT = 10
554
588
 
555
589
  def teardown
590
+ @scratch_watch&.kill
556
591
  @config_restart_unsubscribe&.call
557
592
  @context_poller&.stop
558
593
  drop_queued_commands
@@ -571,6 +606,8 @@ module Samagotchi
571
606
  engine = nil
572
607
  engine = Samagotchi::Engine.new(
573
608
  model_name: @session.model_name,
609
+ # `chi scratch`: no memory_write, no delegates, approvals in memory.
610
+ scratch: @session.scratch,
574
611
  model_typed: @session.model_typed,
575
612
  # The session's --memory and --mute lists: the same prompt on every
576
613
  # (re)spawn.
@@ -783,7 +820,7 @@ module Samagotchi
783
820
  end
784
821
 
785
822
  # A participant's cap in dollars: their own, else the config's.
786
- def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Web::Grants.default_cap
823
+ def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Grants.default_cap
787
824
 
788
825
  # A participant's cleaned name, else their id.
789
826
  def participant_label(author, name)
@@ -830,7 +867,7 @@ module Samagotchi
830
867
  nil
831
868
  end
832
869
 
833
- # A due reminder runs as a continue turn, as in the REPL: Engine#run_turn
870
+ # A due reminder runs as a continue turn: Engine#run_turn
834
871
  # injects the reminders as a tail system message, and no user message is
835
872
  # added. A prompt's turn may have injected them already (a stale latch):
836
873
  # then there is nothing to run.
@@ -1414,7 +1451,7 @@ module Samagotchi
1414
1451
  # WorkerInbound.save_or_log: a failed save between turns is logged.
1415
1452
  def save_or_log(at, &) = WorkerInbound.save_or_log(at, &)
1416
1453
 
1417
- # Back to the conversation before the failed turn, as the REPL does (so
1454
+ # Back to the conversation before the failed turn (so
1418
1455
  # failed prompts don't pile up as consecutive user messages), and each
1419
1456
  # prompt it took (its own and any merged into it) goes back to its
1420
1457
  # sender, who can send it again. The rollback and the announcements are
@@ -1559,6 +1596,57 @@ module Samagotchi
1559
1596
  end
1560
1597
  end
1561
1598
 
1599
+ # How long a scratch worker waits for its terminal's first stream
1600
+ # (AttachLauncher attaches within BRIDGE_WAIT of the spawn).
1601
+ SCRATCH_JOIN_GRACE = 30
1602
+
1603
+ # How long a scratch worker's terminal may have no stream open, after
1604
+ # it had one, before it counts as gone: twice the attached terminal's
1605
+ # whole reconnect ladder (TerminalUI::LocalLink#reconnect_delays), so
1606
+ # a reconnect gap (a dropped stream, a slow worker) never reads as a
1607
+ # terminal that left.
1608
+ SCRATCH_GONE_AFTER = 2 * BridgeClient::EventStream::REDISCOVER_DELAYS.sum
1609
+
1610
+ # Whether this is a `chi scratch` session's worker whose terminal is
1611
+ # gone: no stream open for SCRATCH_GONE_AFTER, after one was (a killed
1612
+ # TUI's closes as its writer reads the hang-up), or none came within
1613
+ # SCRATCH_JOIN_GRACE. Its terminal deletes it on every way out it sees;
1614
+ # this is for the ones it doesn't (kill -9, a closed terminal window).
1615
+ def scratch_unattended?
1616
+ return false unless @session&.scratch && @bridge
1617
+
1618
+ if @bridge.open_streams.positive?
1619
+ @unattended_since = nil
1620
+ return false
1621
+ end
1622
+ return monotonic_now - @started_at >= SCRATCH_JOIN_GRACE unless @bridge.streams_served.positive?
1623
+
1624
+ # Set by the first look (the scratch watch or the idle pass) that
1625
+ # finds no stream; a stream back resets it above.
1626
+ @unattended_since ||= monotonic_now
1627
+ monotonic_now - @unattended_since >= SCRATCH_GONE_AFTER
1628
+ end
1629
+
1630
+ def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1631
+
1632
+ # #leave_idle for a scratch worker its terminal left: checked again with
1633
+ # the event log held, then the intake closes. The session is deleted
1634
+ # once the lock is free (SessionManager.run_session_loop: :scratch_left).
1635
+ # @return [Boolean] false when a client came back since
1636
+ def leave_scratch
1637
+ stop_bridge_after do
1638
+ @engine.synchronize_events do
1639
+ # A turn still running is the scratch watch's to cancel first.
1640
+ next false unless scratch_unattended? && !@engine.turn_running?
1641
+
1642
+ @bridge&.close_intake
1643
+ @engine.stop_idle(join: false)
1644
+ Log.info(:worker, "scratch_left", served: @bridge.streams_served)
1645
+ true
1646
+ end
1647
+ end
1648
+ end
1649
+
1562
1650
  # Check again with the event log held, which the Bridge holds while it
1563
1651
  # queues a POST /turn, then close the Bridge's intake so no client can
1564
1652
  # queue one after the check, and stop the idle jobs (reminder callback,
@@ -1694,6 +1782,9 @@ module Samagotchi
1694
1782
  # @return [WorkerIdleExit::Hold, nil]
1695
1783
  def exit_hold(client_id, kind:, stop:, ask:)
1696
1784
  if kind == :restart
1785
+ # A scratch session's worker ends with its terminal: never restarted.
1786
+ return WorkerIdleExit::Hold.new(reason: :scratch) if @session.scratch
1787
+
1697
1788
  hold = @idle_exit.hold_for_restart
1698
1789
  return hold && WorkerIdleExit::Hold.new(reason: hold)
1699
1790
  end
@@ -1792,7 +1883,7 @@ module Samagotchi
1792
1883
  # A restart keeps even an empty session (a new worker takes it over at
1793
1884
  # once) and leaves the recap to the conversation's next real pause.
1794
1885
  def left(result)
1795
- deleting = result == :exit_requested && @exit_deletes
1886
+ deleting = (result == :exit_requested && @exit_deletes) || result == :scratch_left
1796
1887
  # The attached context goes with this process: a change that landed after
1797
1888
  # the leaving pass looked at it has no later pass to note it, so note it
1798
1889
  # here (no wake turn: the worker is on its way out). Not into a session
@@ -1805,7 +1896,7 @@ module Samagotchi
1805
1896
  # session /exit --delete deletes next: its save could land after the
1806
1897
  # delete and write the session back.
1807
1898
  @task_wakes.absorb unless deleting
1808
- return result if result == :restart
1899
+ return result if %i[restart scratch_left].include?(result)
1809
1900
 
1810
1901
  @discard = empty_session?
1811
1902
  write_recap_on_leave unless @discard || deleting
@@ -23,9 +23,9 @@ module Samagotchi
23
23
  # past START_LIMIT without one;
24
24
  # - Leaving: the worker closed its Bridge and holds the lock for its last work;
25
25
  # - Skew: a worker whose sidecar speaks a newer PROTOCOL than this chi;
26
- # - Stale: a sidecar, but the lock is free (or a chi REPL holds it, which
26
+ # - Stale: a sidecar, but the lock is free (or a one-turn chi run holds it, which
27
27
  # writes none): its worker died;
28
- # - None: no worker and no sidecar, or a chi REPL.
28
+ # - None: no worker and no sidecar, or a one-turn chi run.
29
29
  # A kernel releases a dead worker's lock, so a free lock is the proof of
30
30
  # death a TCP probe never gave (a port can be reused by another session's
31
31
  # Bridge).
@@ -80,7 +80,7 @@ module Samagotchi
80
80
  end
81
81
 
82
82
  # No worker: no sidecar in the session folder, and no worker's lock (a
83
- # chi REPL's, +owner+, is no worker's).
83
+ # one-turn chi run's, +owner+, is no worker's).
84
84
  None = Data.define(:owner) do
85
85
  def initialize(owner: nil) = super
86
86
 
@@ -3,6 +3,7 @@
3
3
  require_relative "working_set"
4
4
  require_relative "working_set_steer"
5
5
  require_relative "tools/memory"
6
+ require_relative "session"
6
7
 
7
8
  module Samagotchi
8
9
  # /working-set: with no arguments the session's working set (WorkingSet,
@@ -50,7 +51,7 @@ module Samagotchi
50
51
  return [listing, [], :ok] if verb.nil?
51
52
  raise ArgumentError, USAGE unless VERBS.include?(verb) && !words.empty?
52
53
 
53
- session = @engine.session or raise ArgumentError, "no session yet"
54
+ session = @engine.session or raise ArgumentError, Session::NONE_YET
54
55
  cwd = session.working_directory
55
56
  strategy = @engine.working_set_strategy
56
57
  messages = @engine.clone_messages(session.messages)
@@ -79,7 +80,7 @@ module Samagotchi
79
80
  # The set under the strategy the next turn runs, and the pending ops.
80
81
  def listing
81
82
  session = @engine.session
82
- return "no session yet" unless session
83
+ return Session::NONE_YET unless session
83
84
 
84
85
  messages = Array(session.messages)
85
86
  items = WorkingSet.build(messages, cwd: session.working_directory, strategy: @engine.working_set_strategy,