samagotchi 0.102.0 → 0.104.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -1
  3. data/bin/chi +60 -191
  4. data/docs/architecture.md +67 -1
  5. data/docs/cli.md +190 -9
  6. data/docs/configuration.md +7 -1
  7. data/docs/guardrails.md +4 -2
  8. data/docs/internals/context-telemetry.md +6 -2
  9. data/docs/internals/engine.md +5 -2
  10. data/docs/internals/llm-context-forget.md +11 -1
  11. data/docs/internals/participants.md +137 -0
  12. data/docs/internals/remote-sessions.md +429 -0
  13. data/docs/internals/working-set.md +95 -11
  14. data/docs/plugins.md +33 -3
  15. data/docs/sessions.md +16 -7
  16. data/docs/sub-agent.md +16 -0
  17. data/docs/testing.md +68 -4
  18. data/lib/samagotchi/answer_command.rb +27 -25
  19. data/lib/samagotchi/bridge.rb +31 -7
  20. data/lib/samagotchi/bridge_client/event_stream.rb +86 -12
  21. data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
  22. data/lib/samagotchi/bridge_client.rb +26 -39
  23. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
  25. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  26. data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
  27. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
  28. data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
  29. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  30. data/lib/samagotchi/chi_invocation.rb +40 -0
  31. data/lib/samagotchi/cli/command.rb +3 -2
  32. data/lib/samagotchi/cli/flags.rb +5 -0
  33. data/lib/samagotchi/cli/parent_wait.rb +22 -13
  34. data/lib/samagotchi/context_command.rb +80 -73
  35. data/lib/samagotchi/context_status.rb +15 -2
  36. data/lib/samagotchi/engine.rb +28 -14
  37. data/lib/samagotchi/guardrail_wiring.rb +7 -1
  38. data/lib/samagotchi/idle_recap.rb +8 -2
  39. data/lib/samagotchi/idle_scheduler.rb +34 -9
  40. data/lib/samagotchi/kernel_loop.rb +21 -2
  41. data/lib/samagotchi/launch_mode.rb +26 -0
  42. data/lib/samagotchi/llm/chat_loop.rb +8 -1
  43. data/lib/samagotchi/llm_context_forget.rb +13 -6
  44. data/lib/samagotchi/note_command.rb +8 -7
  45. data/lib/samagotchi/owner_lock.rb +65 -5
  46. data/lib/samagotchi/parent_report.rb +55 -43
  47. data/lib/samagotchi/participants.rb +122 -0
  48. data/lib/samagotchi/remote_attach_command.rb +119 -0
  49. data/lib/samagotchi/remote_command.rb +170 -0
  50. data/lib/samagotchi/remotes.rb +130 -0
  51. data/lib/samagotchi/run_options.rb +245 -0
  52. data/lib/samagotchi/send_command/busy.rb +58 -0
  53. data/lib/samagotchi/send_command/parent_gate.rb +23 -10
  54. data/lib/samagotchi/send_command/wait_target.rb +16 -3
  55. data/lib/samagotchi/send_command.rb +173 -108
  56. data/lib/samagotchi/session.rb +38 -9
  57. data/lib/samagotchi/session_access/contract.rb +167 -0
  58. data/lib/samagotchi/session_access/dispatch.rb +152 -0
  59. data/lib/samagotchi/session_access/http_transport.rb +109 -0
  60. data/lib/samagotchi/session_access/local.rb +400 -0
  61. data/lib/samagotchi/session_access/remote.rb +734 -0
  62. data/lib/samagotchi/session_access/web_bridge_client.rb +236 -0
  63. data/lib/samagotchi/session_access.rb +223 -0
  64. data/lib/samagotchi/session_archive_command.rb +9 -5
  65. data/lib/samagotchi/session_commands.rb +20 -15
  66. data/lib/samagotchi/session_continue.rb +1 -1
  67. data/lib/samagotchi/session_delete_command.rb +8 -4
  68. data/lib/samagotchi/session_inbox.rb +18 -9
  69. data/lib/samagotchi/session_manager.rb +27 -6
  70. data/lib/samagotchi/session_setup.rb +11 -6
  71. data/lib/samagotchi/sessions_command.rb +36 -14
  72. data/lib/samagotchi/share_command.rb +174 -0
  73. data/lib/samagotchi/terminal_ui/attach_launcher.rb +66 -17
  74. data/lib/samagotchi/terminal_ui/attach_link.rb +114 -0
  75. data/lib/samagotchi/terminal_ui/attached_loop.rb +155 -87
  76. data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
  77. data/lib/samagotchi/terminal_ui/local_link.rb +135 -0
  78. data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
  79. data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
  80. data/lib/samagotchi/terminal_ui/version_lines.rb +28 -3
  81. data/lib/samagotchi/terminal_ui.rb +6 -3
  82. data/lib/samagotchi/testing/waiting.rb +92 -9
  83. data/lib/samagotchi/title_command.rb +9 -11
  84. data/lib/samagotchi/tools/task_runtime.rb +17 -3
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +530 -126
  87. data/lib/samagotchi/web/grants.rb +302 -0
  88. data/lib/samagotchi/web/participant_access.rb +286 -0
  89. data/lib/samagotchi/web/principal.rb +29 -0
  90. data/lib/samagotchi/web/public/data.js +3 -2
  91. data/lib/samagotchi/web/public/index.html +20 -4
  92. data/lib/samagotchi/web/public/stage_frame.js +7 -2
  93. data/lib/samagotchi/web/public/stage_view.js +3 -2
  94. data/lib/samagotchi/web/public/stream_handlers.js +3 -1
  95. data/lib/samagotchi/web/public/working_set.js +159 -28
  96. data/lib/samagotchi/web/public/working_set_rail.js +39 -12
  97. data/lib/samagotchi/web/public/working_set_view.js +91 -33
  98. data/lib/samagotchi/web/server.rb +3 -1
  99. data/lib/samagotchi/web/session_access_routes.rb +459 -0
  100. data/lib/samagotchi/worker.rb +26 -5
  101. data/lib/samagotchi/worker_sidecar.rb +12 -5
  102. data/lib/samagotchi/working_set.rb +88 -14
  103. data/lib/samagotchi/working_set_command.rb +133 -30
  104. data/lib/samagotchi/working_set_pin.rb +34 -0
  105. data/lib/samagotchi/working_set_steer.rb +191 -27
  106. metadata +28 -1
@@ -5,10 +5,19 @@
5
5
  // and the step tags' jump to a call. The rail is working_set_rail.js's; the
6
6
  // rules are working_set.js's.
7
7
  //
8
- // Steering (P3a: drop / undrop): the list's rows and groups post
9
- // `/working-set drop|undrop KEY…` lines (+sendLine+, the session command
10
- // route). Between turns one runs at once: its command_ran re-reads the
11
- // conversation and the re-emitted set strikes the item `dropped`. While a
8
+ // Steering (drop / undrop, pin / unpin, + add): the list's rows and groups post
9
+ // `/working-set drop|undrop|pin|unpin|add KEY…` lines (+sendLine+, the session
10
+ // command route). The list's foot (the sheet's last row) adds what the user
11
+ // types (a path, memory:NAME or a URL); a stale or forgotten row offers
12
+ // refresh (an add of it). An add is pending until the next turn sends its
13
+ // note: the server's snapshot lists it (adds) between turns, a queued line
14
+ // mid-turn; an added item the set doesn't hold yet is a `+ name` row with
15
+ // `reads next turn` and an undo (unadd). Between turns one runs at once: its command_ran re-reads the
16
+ // conversation and the re-emitted set strikes the item `dropped` (or marks
17
+ // it pinned). A row offers pin only when the session's strategy has the
18
+ // forget layer (forgetOn: pins guard against the model's forget); a pinned
19
+ // row shows its 📌 and unpin under any strategy, since its drop waits for
20
+ // the unpin. While a
12
21
  // turn runs it is queued: its command_queued (waits: "turn_end"; any
13
22
  // client's, and the snapshot's replay after a reload) makes it pending, a
14
23
  // dashed-warn row with a pill and an undo (the inverse line, queued after
@@ -32,8 +41,9 @@ import { html, nothing, render, svg } from "./vendor/lit-html.js";
32
41
  import { createChipPopover } from "./chip_popover.js";
33
42
  import { showRow } from "./refs.js";
34
43
  import {
35
- BUDGETS, dropRefusal, fold, footprint, footprintText, GLYPHS, INVERSE, isFresh, itemsOfTurn, labelText, labelTitle, newerSnapshot,
36
- opsText, overlay, parseSteerLine, pendingSteers, sessionItems, steerId, steerLine, stepRange, stepTag, tokensText,
44
+ addedItems, addLine, BUDGETS, dropRefusal, fold, footprint, footprintText, forgetOn, GLYPHS, INVERSE, isFresh, itemsOfTurn, labelText,
45
+ labelTitle, newerSnapshot, opsText, overlay, parseSteerLine, pendingSteers, pinnable, refreshable, serverAddIds, sessionItems, STEER_OPS,
46
+ steerId, steerLine, stepRange, stepTag, tokensText, undroppable,
37
47
  } from "./working_set.js";
38
48
 
39
49
  // The popover's files fold to this many lines (groups open in place, one at
@@ -194,38 +204,69 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
194
204
  if (opening) fetchServer();
195
205
  },
196
206
  onClose: () => { openGroup = null; onChange(); },
207
+ // The foot's + add (the popover's submit): one KEY as typed (quoted
208
+ // when it has spaces).
209
+ onSubmit: (form) => {
210
+ if (!form?.matches?.(".ws-add")) return;
211
+ const input = form.querySelector(".ws-add-in");
212
+ const line = sendLine && addLine(input?.value);
213
+ if (!line) return;
214
+ input.value = "";
215
+ Promise.resolve(sendLine(line)).catch(() => {});
216
+ },
197
217
  });
198
218
 
199
- const pending = () => pendingSteers([...queued.values()]);
219
+ // The pending ops: the server's adds (made between turns), then the
220
+ // queued lines.
221
+ const pending = () => pendingSteers([...queued.values()], state().dir, serverAddIds(server));
222
+ // The pending adds the set doesn't hold yet, as rows.
223
+ const adding = (pend = pending()) => addedItems(pend, sessionSet().items, server, state().dir);
200
224
 
201
225
  // A steer button: +op+ on +items+ ("undo" sends the inverse of a pending op).
202
226
  function actButton(op, items, { label = op, title = "", disabled = false } = {}) {
203
- const ids = items.map(steerId).join(" ");
227
+ // JSON: an id may hold spaces (a file under "my notes/").
228
+ const ids = JSON.stringify(items.map(steerId));
204
229
  return html`<button type="button" class=${`ws-act-b ${op}`} data-ws="steer" data-op=${op} data-ids=${ids} title=${title} ?disabled=${disabled}>${label}</button>`;
205
230
  }
206
231
 
232
+ // A pending op's pill.
233
+ const PILLS = { drop: "drops next turn", undrop: "back next turn", pin: "applies next turn", unpin: "applies next turn",
234
+ add: "reads next turn" };
235
+
207
236
  // What a row offers: a pending op's pill and undo, an undrop, a drop (or
208
- // why not); nothing for an item no op applies to.
209
- function rowActions(item, pend) {
237
+ // why not), a pin or an unpin; nothing for an item no op applies to.
238
+ function rowActions(item, pend, forget) {
210
239
  if (!sendLine) return { pill: nothing, buttons: nothing };
211
- const op = pend.get(steerId(item));
240
+ let op = pend.get(steerId(item));
241
+ // A drop queued for a pinned item: the worker refuses it (unpin it
242
+ // first), so it isn't pending; the row offers its greyed drop.
243
+ if (op === "drop" && item.pinned) op = undefined;
212
244
  if (op) {
213
- const pill = html`<span class="ws-pend" title="applies at the next turn's start">${op === "drop" ? "drops next turn" : "back next turn"}</span>`;
245
+ const when = op === "add" ? "the next turn's note asks the model to read it first" : "applies at the next turn's start";
246
+ const pill = html`<span class="ws-pend" title=${when}>${PILLS[op]}</span>`;
214
247
  return { pill, buttons: actButton(INVERSE[op], [item], { label: "undo", title: `take back the queued ${op}` }) };
215
248
  }
216
- if (item.state === "dropped") {
217
- return { pill: nothing, buttons: actButton("undrop", [item], { title: "send it whole again from the next request (a cache break)" }) };
218
- }
249
+ const undrop = actButton("undrop", [item], { title: "send it whole again from the next request (a cache break)" });
250
+ // Dropped, or a drop on some reads with nothing else of it sent (the
251
+ // model's layers stubbed the rest): undrop is all it offers.
252
+ if (item.state === "dropped" || (undroppable(item) && item.state === "forgotten")) return { pill: nothing, buttons: undrop };
253
+ const buttons = [];
219
254
  const why = dropRefusal(item);
220
- if (why === null) return { pill: nothing, buttons: nothing };
221
- return { pill: nothing, buttons: actButton("drop", [item], { title: why || "send a stub instead of its reads from the next turn's start", disabled: !!why }) };
255
+ if (why !== null) buttons.push(actButton("drop", [item], { title: why || "send a stub instead of its reads from the next turn's start", disabled: !!why }));
256
+ if (item.pinned) buttons.push(actButton("unpin", [item], { title: "the model's forget_outputs may forget it again" }));
257
+ else if (forget && pinnable(item)) buttons.push(actButton("pin", [item], { title: "keep it from the model's forget_outputs (its later reads too)" }));
258
+ // A drop on some of its reads, others still sent: undrop takes it back.
259
+ if (undroppable(item)) buttons.push(undrop);
260
+ if (refreshable(item)) buttons.push(actButton("add", [item], { label: "refresh", title: "the next turn's note asks the model to read it again" }));
261
+ return { pill: nothing, buttons: buttons.length ? buttons : nothing };
222
262
  }
223
263
 
224
- function rowTemplate(item, { session: inSession, group = null, now, pend = new Map() }) {
264
+ function rowTemplate(item, { session: inSession, group = null, now, pend = new Map(), forget = false }) {
225
265
  const fresh = isFresh(seenAt(item), now);
226
266
  const tag = stepTag(item, { session: inSession });
227
- const words = `${opsText(item)}${fresh ? " · just now" : ""}`;
228
- const glyph = item.muted ? GLYPHS.muted : fresh ? "●" : GLYPHS[item.kind] || "";
267
+ // An added row's words: what it is (a phone's tap on them opens its strip).
268
+ const words = item.added ? "added" : `${opsText(item)}${fresh ? " · just now" : ""}`;
269
+ const glyph = item.added ? "+" : item.muted ? GLYPHS.muted : fresh ? "●" : GLYPHS[item.kind] || "";
229
270
  const name = memberName(item, group);
230
271
  const full = item.kind === "file" ? item.path || item.key : item.kind === "url" ? item.url || item.key : item.key;
231
272
  let nameEl;
@@ -248,11 +289,12 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
248
289
  const state = item.state && item.state !== "live" ? ` ${item.state}` : "";
249
290
  const tokens = tokensText(item.tokens);
250
291
  const id = steerId(item);
251
- const { pill, buttons } = rowActions(item, pend);
292
+ const { pill, buttons } = rowActions(item, pend, forget);
293
+ const pin = item.pinned ? html`<span class="ws-pin" title="pinned: the model's forget_outputs refuses it">📌</span>` : nothing;
252
294
  const isPending = pill !== nothing;
253
295
  const strip = stripOpen === id && buttons !== nothing;
254
296
  const open = item.kind === "file" ? html`<button type="button" class="ws-act-b open ref" data-ref-kind="file" data-ref-path=${item.ref?.path || full}>open</button>` : nothing;
255
- const row = html`<div class=${`ws-row ${item.kind}${fresh ? " fresh" : ""}${item.muted ? " muted" : ""}${group ? " member" : ""}${state}${isPending ? " pending" : ""}${strip ? " strip-open" : ""}`} data-ws-id=${id}><span class="k">${glyph}</span>${nameEl}${edited ? html`<span class="e">✎</span>` : nothing}<span class="st">${words}</span>${pill}${tokens ? html`<span class="tk" title=${`about ${item.tokens} tokens of it are sent`}>${tokens}</span>` : nothing}${tagEl}${buttons !== nothing ? html`<span class="ws-act">${buttons}</span>` : nothing}</div>`;
297
+ const row = html`<div class=${`ws-row ${item.kind}${item.added ? " added" : ""}${fresh ? " fresh" : ""}${item.muted ? " muted" : ""}${group ? " member" : ""}${state}${isPending ? " pending" : ""}${strip ? " strip-open" : ""}`} data-ws-id=${id}><span class="k">${glyph}</span>${nameEl}${pin}${edited ? html`<span class="e">✎</span>` : nothing}<span class="st">${words}</span>${pill}${tokens ? html`<span class="tk" title=${`about ${item.tokens} tokens of it are sent`}>${tokens}</span>` : nothing}${tagEl}${buttons !== nothing ? html`<span class="ws-act">${buttons}</span>` : nothing}</div>`;
256
298
  // One template either way, so the tapped row stays the same node (a
257
299
  // click outside the popover closes it).
258
300
  return html`${row}${strip ? html`<div class="ws-strip">${buttons}${open}</div>` : nothing}`;
@@ -282,7 +324,8 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
282
324
  const memories = set.items.filter((i) => i.kind === "memory");
283
325
  const urls = set.items.filter((i) => i.kind === "url");
284
326
  const pend = pending();
285
- const opts = { session: inSession, now, pend };
327
+ const forget = forgetOn(st.llmContext);
328
+ const opts = { session: inSession, now, pend, forget };
286
329
  const fileLabels = fold(files, POP_BUDGET).labels;
287
330
  // Memories are rows of their own; past the budget one group that opens.
288
331
  const memoryLabels = fold(memories, memories.length > POP_BUDGET ? 1 : Infinity).labels;
@@ -290,14 +333,17 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
290
333
  const memMeta = [`${memories.length - preloaded} used`, preloaded ? `${preloaded} preloaded` : "", set.muted.length ? `${set.muted.length} muted` : ""].filter(Boolean).join(" · ");
291
334
  const turnLabel = view.turn !== null && view.turn >= 0 ? `turn ${view.turn + 1}` : "this turn";
292
335
  const pct = Number.isFinite(st.ctxPct) ? ` · ctx ${Math.round(st.ctxPct)} %` : "";
293
- const empty = !set.items.length && !set.muted.length;
336
+ const added = sendLine ? adding(pend) : [];
337
+ const empty = !set.items.length && !set.muted.length && !added.length;
294
338
  const tpl = html`<div class="ctx-pop-head"><strong>working set</strong><span class="ws-scope" role="group" aria-label="Scope"><button type="button" class=${!inSession ? "on" : ""} aria-pressed=${String(!inSession)} data-ws="scope" data-scope="turn" ?disabled=${view.turn === null || view.turn < 0}>${turnLabel}</button><button type="button" class=${inSession ? "on" : ""} aria-pressed=${String(inSession)} data-ws="scope" data-scope="session">session</button></span><span class="ctx-pop-kind">${set.items.length} item${set.items.length === 1 ? "" : "s"}${pct}</span>${ringTemplate(st.ctxPct)}<button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>
295
339
  ${empty ? html`<div class="ctx-pop-meta">Nothing yet: the files, memories and URLs ${inSession ? "this session's turns read" : "this turn reads"} show here.</div>` : nothing}
296
340
  ${files.length ? html`<div class="ctx-pop-meta">files · ${files.length}</div>${labelsTemplate(fileLabels, opts)}` : nothing}
297
341
  ${memories.length || set.muted.length ? html`<div class="ctx-pop-meta">memories${memMeta ? ` · ${memMeta}` : ""}</div>${labelsTemplate(memoryLabels, opts)}${set.muted.map((m) => rowTemplate(m, opts))}` : nothing}
298
342
  ${urls.length ? html`<div class="ctx-pop-meta">urls · ${urls.length}</div>${labelsTemplate(fold(urls, POP_BUDGET).labels, opts)}` : nothing}
299
- ${pend.size ? html`<div class="ws-pending" role="status">${pend.size} change${pend.size === 1 ? "" : "s"} pending · apply at the next turn's start · <button type="button" class="ws-act-b" data-ws="undo-all">undo all</button></div>` : nothing}
300
- <div class="ctx-pop-meta ws-hint">A file opens its ref; a step tag goes to the call.${sendLine ? " Drop sends stubs instead of an item's reads from the next turn." : ""} Esc closes.</div>`;
343
+ ${added.length ? html`<div class="ctx-pop-meta">adding · ${added.length}</div>${added.map((it) => rowTemplate(it, opts))}` : nothing}
344
+ ${pend.size ? html`<div class="ws-pending" role="status"><span class="ws-pending-t">${pend.size} change${pend.size === 1 ? "" : "s"} pending<span class="ws-pending-when"> · apply at the next turn's start</span></span><button type="button" class="ws-act-b" data-ws="undo-all">undo all</button></div>` : nothing}
345
+ ${sendLine ? html`<form class="ws-add" data-ws="add-form"><input class="ws-add-in" type="text" name="key" placeholder="+ path, memory:NAME or URL" aria-label="Add to the working set: a path, memory:NAME or a URL" autocomplete="off" autocapitalize="off" spellcheck="false" enterkeyhint="go"><button type="submit" class="ws-act-b">add</button></form>` : nothing}
346
+ <div class="ctx-pop-meta ws-hint">A file opens its ref; a step tag goes to the call.${sendLine ? " Drop sends stubs instead of an item's reads from the next turn; add asks the model to read one first." : ""}${sendLine && forget ? " Pin keeps an item from the model's forget." : ""} Esc closes.</div>`;
301
347
  render(tpl, popover.pop);
302
348
  }
303
349
 
@@ -329,10 +375,10 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
329
375
  // Posts +op+ for the ids (the items' steer words, from the set).
330
376
  function steer(op, ids) {
331
377
  if (!sendLine || !ids.length) return;
332
- const all = [...sessionSet().items, ...turnItems(view.turn)];
378
+ const all = [...sessionSet().items, ...turnItems(view.turn), ...adding()];
333
379
  const byId = new Map(all.map((it) => [steerId(it), it]));
334
380
  const items = ids.map((id) => byId.get(id)).filter(Boolean);
335
- if (items.length) Promise.resolve(sendLine(steerLine(op, items))).catch(() => {});
381
+ if (items.length) Promise.resolve(sendLine(steerLine(op, items, state().dir))).catch(() => {});
336
382
  }
337
383
 
338
384
  popover.pop.addEventListener("click", (e) => {
@@ -349,10 +395,10 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
349
395
  }
350
396
  if (!act) return;
351
397
  if (act.dataset.ws === "steer") {
352
- steer(act.dataset.op, act.dataset.ids.split(" ").filter(Boolean));
398
+ steer(act.dataset.op, JSON.parse(act.dataset.ids || "[]"));
353
399
  } else if (act.dataset.ws === "undo-all") {
354
400
  const pend = pending();
355
- for (const op of ["drop", "undrop"]) {
401
+ for (const op of STEER_OPS) {
356
402
  const ids = [...pend].filter(([, o]) => o === op).map(([id]) => id);
357
403
  steer(INVERSE[op], ids);
358
404
  }
@@ -398,6 +444,12 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
398
444
  if (popover.isOpen()) popover.refresh();
399
445
  }
400
446
 
447
+ // The pending ops changed: the list and the rail's marks.
448
+ function pendingChanged() {
449
+ changed();
450
+ for (const fn of listeners) fn();
451
+ }
452
+
401
453
  // The footprints on the page (the history's, the hand-off's) drawn again
402
454
  // in place when their items' states changed: the same node (its focus,
403
455
  // the popover's opener), its children and label replaced.
@@ -466,12 +518,18 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
466
518
  refreshFootprints();
467
519
  fetchServer();
468
520
  },
521
+ // The session's strategy changed (/llm-context's command_ran): the open
522
+ // list redraws, its pin buttons shown only under forget (forgetOn).
523
+ strategyChanged() {
524
+ if (popover.isOpen()) popover.refresh();
525
+ },
469
526
  // A :working_set_updated event's set ({items, at}).
470
527
  update(snapshot) {
471
528
  events += 1;
472
529
  takeServer(snapshot, { event: true });
473
530
  },
474
- // +fn()+ runs after each new server snapshot (the rail's redraw).
531
+ // +fn()+ runs after each new server snapshot and each change of the
532
+ // pending ops (the rail's redraw).
475
533
  onServer(fn) { listeners.add(fn); },
476
534
  // A command queued for after the running turn (its command_queued,
477
535
  // waits "turn_end"): a steer line is pending until its command_ran.
@@ -482,11 +540,11 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
482
540
  }
483
541
  if (event?.waits !== "turn_end" || !event.command_id || !parseSteerLine(event.line)) return;
484
542
  queued.set(event.command_id, event.line);
485
- changed();
543
+ pendingChanged();
486
544
  },
487
545
  // A command ran (or was dropped): its line is no longer pending.
488
546
  commandRan(event) {
489
- if (event?.command_id && queued.delete(event.command_id)) changed();
547
+ if (event?.command_id && queued.delete(event.command_id)) pendingChanged();
490
548
  },
491
549
  // The steer lines pending (for the specs and the rail).
492
550
  pending: () => pending(),
@@ -95,6 +95,8 @@ module Samagotchi
95
95
  puts lan_off_line(setting)
96
96
  return 0
97
97
  end
98
+ # The participants' grants: never served from a store others can read.
99
+ Grants.check_permissions!(Grants.path)
98
100
  if start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, view: view,
99
101
  annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set)
100
102
  0
@@ -102,7 +104,7 @@ module Samagotchi
102
104
  1
103
105
  end
104
106
  end
105
- rescue Lan::Error => e
107
+ rescue Lan::Error, Grants::Insecure => e
106
108
  warn "Error: #{e.message}"
107
109
  1
108
110
  rescue Interrupt