samagotchi 0.121.1 → 0.122.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/bin/chi +4 -1
  4. data/docs/architecture.md +2 -1
  5. data/docs/cli.md +1 -0
  6. data/docs/configuration.md +29 -4
  7. data/docs/guardrails.md +15 -0
  8. data/docs/internals/engine.md +2 -0
  9. data/docs/internals/participants.md +13 -7
  10. data/docs/memory.md +33 -0
  11. data/docs/plugins.md +3 -2
  12. data/docs/sessions.md +8 -2
  13. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
  14. data/lib/samagotchi/bridge.rb +4 -3
  15. data/lib/samagotchi/bridge_client/event_stream.rb +5 -1
  16. data/lib/samagotchi/bridge_client/stream_open.rb +8 -2
  17. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  18. data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
  19. data/lib/samagotchi/config.rb +5 -0
  20. data/lib/samagotchi/engine.rb +107 -1
  21. data/lib/samagotchi/event_fanout.rb +0 -1
  22. data/lib/samagotchi/guardrail_wiring.rb +5 -4
  23. data/lib/samagotchi/log_line.rb +1 -1
  24. data/lib/samagotchi/memory_orphans.rb +11 -0
  25. data/lib/samagotchi/participant_mode.rb +11 -0
  26. data/lib/samagotchi/plugin/api.rb +7 -4
  27. data/lib/samagotchi/process_group.rb +13 -0
  28. data/lib/samagotchi/review_start.rb +6 -2
  29. data/lib/samagotchi/run_options.rb +1 -0
  30. data/lib/samagotchi/session.rb +14 -1
  31. data/lib/samagotchi/session_access/contract.rb +1 -1
  32. data/lib/samagotchi/session_access/remote.rb +32 -12
  33. data/lib/samagotchi/session_access/web_bridge_client.rb +9 -2
  34. data/lib/samagotchi/session_manager.rb +21 -7
  35. data/lib/samagotchi/session_setup.rb +8 -4
  36. data/lib/samagotchi/sessions_command.rb +1 -1
  37. data/lib/samagotchi/steer.rb +7 -6
  38. data/lib/samagotchi/system_one/client.rb +6 -5
  39. data/lib/samagotchi/system_one/log.rb +25 -5
  40. data/lib/samagotchi/terminal_ui.rb +2 -1
  41. data/lib/samagotchi/tool_runner.rb +2 -1
  42. data/lib/samagotchi/toolkit.rb +27 -8
  43. data/lib/samagotchi/toolkit_binding.rb +115 -7
  44. data/lib/samagotchi/toolkit_command.rb +15 -3
  45. data/lib/samagotchi/toolkit_report_command.rb +318 -0
  46. data/lib/samagotchi/toolkit_suggest.rb +211 -0
  47. data/lib/samagotchi/tools/memory.rb +1 -1
  48. data/lib/samagotchi/version.rb +1 -1
  49. data/lib/samagotchi/web/app.rb +5 -5
  50. data/lib/samagotchi/web/participant_access.rb +4 -1
  51. data/lib/samagotchi/web/participant_review.rb +6 -4
  52. data/lib/samagotchi/web/principal.rb +3 -4
  53. data/lib/samagotchi/web/public/bubbles.js +3 -2
  54. data/lib/samagotchi/web/public/participant.js +2 -1
  55. data/lib/samagotchi/web/public/turn_events.js +2 -1
  56. data/lib/samagotchi/web/public/turn_html.js +1 -1
  57. data/lib/samagotchi/web/server.rb +2 -2
  58. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  59. data/lib/samagotchi/web/session_control.rb +4 -2
  60. data/lib/samagotchi/web/start_dir.rb +10 -5
  61. data/lib/samagotchi/worker.rb +16 -2
  62. data/lib/samagotchi/worker_link.rb +1 -0
  63. metadata +4 -1
@@ -160,9 +160,9 @@ export function createBubbles({
160
160
  // The bubble a reload draws (turn_html.js), with this page's own fields.
161
161
  // +delivery+: how it went into a running turn (next_step, cut, queue): its
162
162
  // queued badge says what it waits for.
163
- function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null, delivery = null, author = null } = {}) {
163
+ function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null, delivery = null, author = null, turnId = null } = {}) {
164
164
  removeHint();
165
- const div = elementOf(userBubbleTemplate({ content: prompt }, {
165
+ const div = elementOf(userBubbleTemplate({ content: prompt, turn_id: turnId }, {
166
166
  label, state, own, enqueuedId, delivery, author, key: normalize(prompt), thumbs: thumbsTemplate(selected(), images),
167
167
  }));
168
168
  return placeFollowing(div);
@@ -224,6 +224,7 @@ export function createBubbles({
224
224
  else liveTurn.unmatchedMerge = true;
225
225
  }
226
226
  }
227
+ if (started && event.type === "turn_started" && event.turn_id) started.dataset.turnId = event.turn_id;
227
228
  return started;
228
229
  }
229
230
 
@@ -277,6 +277,7 @@ function messageNode(message) {
277
277
  const when = fullTime(message.at);
278
278
  if (when) node.title = when;
279
279
  if (user) {
280
+ if (message.turn_id) node.dataset.turnId = message.turn_id;
280
281
  node.style.setProperty("--author-color", authorColor(message.author));
281
282
  // Whose it is, for an annotation's label (participant_annotate.js).
282
283
  Object.assign(node.dataset, messageAuthorData(message));
@@ -382,7 +383,7 @@ function currentTurnNodes() {
382
383
  const nodes = [];
383
384
  if (turn.prompt !== undefined || turn.automatic) {
384
385
  nodes.push(messageNode({ role: "user", content: turn.automatic ? "An automatic turn" : turn.prompt, author: turn.author,
385
- author_name: turn.author_name, mine: turn.mine, automatic: turn.automatic, at: turn.at }));
386
+ author_name: turn.author_name, mine: turn.mine, automatic: turn.automatic, at: turn.at, turn_id: turn.turn_id }));
386
387
  }
387
388
  const reply = el("div", "p-msg reply live");
388
389
  fillLive(reply, $("liveReply"));
@@ -360,11 +360,12 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
360
360
  // The run's cut wait (TurnAccumulator, as turn_started carried it): the
361
361
  // composer's delivery line reads it.
362
362
  const cutAfter = turn.cut_after != null ? { cut_after: turn.cut_after } : {};
363
+ const turnId = turn.turn_id ? { turn_id: turn.turn_id } : {};
363
364
  // A participant's turn names them (TurnAccumulator keeps author, author_name).
364
365
  const author = turn.author ? { author: turn.author, ...(turn.author_name ? { author_name: turn.author_name } : {}) } : {};
365
366
  // Its wall time (TurnAccumulator keeps turn_started's at): the gap divider before it.
366
367
  const at = typeof turn.at === "string" ? { at: turn.at } : {};
367
- events.push(withImages({ type: "turn_started", prompt: turn.prompt, origin: turn.origin, continue: !!turn.continue, started_at, ...at, ...cutAfter, ...author }, turn.images));
368
+ events.push(withImages({ type: "turn_started", prompt: turn.prompt, origin: turn.origin, continue: !!turn.continue, started_at, ...at, ...cutAfter, ...turnId, ...author }, turn.images));
368
369
  let textIteration = null; // a text bubble is open for this iteration
369
370
  const closeText = () => {
370
371
  if (textIteration !== null) events.push({ type: "generation_completed" });
@@ -313,7 +313,7 @@ export function userBubbleTemplate(message, { label = null, state = null, step =
313
313
  const from = author
314
314
  ? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name)}</span>`
315
315
  : label ? html`<span class="origin-label">${label}</span>` : nothing;
316
- return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named, delivery)}</div>`;
316
+ return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named, delivery)}</div>`;
317
317
  }
318
318
 
319
319
  // The notice of a turn that ended with no answer (the server's
@@ -86,9 +86,9 @@ module Samagotchi
86
86
  def self.launch(port: nil, host: nil, scope: "project", start_dir: nil, open_browser: false, markdown: false,
87
87
  view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
88
88
  editor: Config::BY_KEY["web.editor"].default, new_token: false,
89
- working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil)
89
+ working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil, start_dir_from: :flag)
90
90
  start_given = !start_dir.to_s.strip.empty?
91
- start_dir = StartDir.resolve(start_dir)
91
+ start_dir = StartDir.resolve(start_dir, from: start_dir_from)
92
92
  rotate_token if new_token
93
93
  public_url = public_url_setting unless tunnel
94
94
  setting = host_setting(host)
@@ -449,7 +449,7 @@ module Samagotchi
449
449
  # hint}: new text for the session's --push source (chi context push;
450
450
  # the fields as a source's JSON output gives them, checked the same
451
451
  # way). 200 {status: "pushed", name, before, revision, changed}; 404
452
- # not_found (no such source); 409 not_push (a command source); 400
452
+ # context_missing (no such source); 409 not_push (a command source); 400
453
453
  # invalid_text.
454
454
  def handle_context_push(req, id, name)
455
455
  body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
@@ -462,13 +462,13 @@ module Samagotchi
462
462
  end
463
463
  pushed = session_access.context_push(context_target(id), name, fetched)
464
464
  case pushed.status
465
- when :missing then error_response(404, "not_found", "no source #{name[0, 40]}")
465
+ when :missing then error_response(404, "context_missing", "no source #{name[0, 40]}")
466
466
  when :not_push then error_response(409, "not_push", SessionAccess::ContextPush.not_push_message(name))
467
467
  else json_response(200, { status: "pushed", name: name, before: pushed.before, revision: pushed.revision,
468
468
  changed: pushed.before != pushed.revision })
469
469
  end
470
470
  rescue ContextSources::Invalid
471
- error_response(404, "not_found", "no source #{name[0, 40]}")
471
+ error_response(404, "context_missing", "no source #{name[0, 40]}")
472
472
  end
473
473
 
474
474
  # The session as chi context's Target (SessionAccess::Local's context
@@ -423,7 +423,8 @@ module Samagotchi
423
423
  # the newest chi installed (SessionManager.restart_session). 200
424
424
  # {status: "restarted", session_id, from_version, version} (version nil:
425
425
  # the new worker is still starting; its bridge_up follows); 409
426
- # {error: "held" | "not_running" | "unsupported" | "failed", reason, detail} in words; 409
426
+ # {error: "held" | "not_running" | "unsupported" | "failed", reason, detail} in words (a stopped
427
+ # worker: held, reason stopped, its pid); 409
427
428
  # owned_by_tui; 404.
428
429
  def handle_restart(_req, id)
429
430
  result = @manager.restart_session(id, state_dir: @state_dir, client_id: ClientId::WEB_RESTART)
@@ -432,7 +433,8 @@ module Samagotchi
432
433
  version: result.version })
433
434
  rescue SessionManager::RestartRefused => e
434
435
  error = %i[not_running unsupported failed].include?(e.reason) ? e.reason.to_s : "held"
435
- refusal_response(409, error, e, reason: e.reason.to_s, session_id: id, version: e.version)
436
+ refusal_response(409, error, e, reason: e.reason.to_s, session_id: id, version: e.version, holds: e.holds,
437
+ pid: e.pid)
436
438
  rescue SessionManager::OwnedByTUI => e
437
439
  owned_by_tui(e)
438
440
  end
@@ -9,15 +9,20 @@ module Samagotchi
9
9
  module StartDir
10
10
  class Invalid < StandardError; end
11
11
 
12
+ # Where +given+ came from, as its error names it.
13
+ SOURCES = { flag: "--start-dir", config: "web.start_dir in config.yml" }.freeze
14
+
12
15
  # +given+ as an absolute folder (~ expanded, a relative one against
13
- # +cwd+); nil or blank: +cwd+.
16
+ # +cwd+, or the home folder for a config value); nil or blank: +cwd+.
17
+ # @param from [Symbol] :flag or :config (SOURCES)
14
18
  # @raise [Invalid] when it is no folder
15
- def self.resolve(given, cwd: Dir.pwd)
19
+ def self.resolve(given, cwd: Dir.pwd, from: :flag)
16
20
  return File.expand_path(cwd) if given.nil? || given.to_s.strip.empty?
17
21
 
18
- dir = File.expand_path(given.to_s.strip, cwd)
19
- raise Invalid, "--start-dir #{given}: no such folder" unless File.exist?(dir)
20
- raise Invalid, "--start-dir #{given}: not a folder" unless File.directory?(dir)
22
+ source = SOURCES.fetch(from)
23
+ dir = File.expand_path(given.to_s.strip, from == :config ? Dir.home : cwd)
24
+ raise Invalid, "#{source} #{given}: no such folder" unless File.exist?(dir)
25
+ raise Invalid, "#{source} #{given}: not a folder" unless File.directory?(dir)
21
26
 
22
27
  dir
23
28
  end
@@ -519,11 +519,19 @@ module Samagotchi
519
519
  # it as waiting, and the next worker drops it (drop_dead_question).
520
520
  # The context poller first: a command it runs goes (its process
521
521
  # group) before the Engine does.
522
+ # How long the lock stays held once #teardown marks it leaving (an upper
523
+ # bound): the recap on leave is written by then (#left), what is left is
524
+ # Bridge#stop's graces and ChildRing.await_wakes.
525
+ TEARDOWN_LEAVE_WAIT = 10
526
+
522
527
  def teardown
523
528
  @config_restart_unsubscribe&.call
524
529
  @context_poller&.stop
525
530
  drop_queued_commands
526
531
  @engine&.shutdown
532
+ # Leaving before the Bridge closes: a client that finds its port shut
533
+ # from here on reads Leaving (and waits for the lock), not Live.
534
+ mark_leaving(wait: TEARDOWN_LEAVE_WAIT)
527
535
  @bridge&.stop
528
536
  # The process exits next: a parent wake it rang for (a crash's
529
537
  # ring comes just above) must spawn the parent's worker first.
@@ -1438,8 +1446,14 @@ module Samagotchi
1438
1446
  # How long the lock stays held after the Bridge: the recap on leave's
1439
1447
  # timeout (#left), none for a restart (#left writes none). A delete
1440
1448
  # writes none either, but the bound only has to be an upper one.
1441
- def mark_leaving
1442
- @owner_lock&.mark_leaving(wait: @exit_restart ? 0 : @engine.recap_timeout.to_f)
1449
+ # Once: the teardown's (after #stop_bridge_after's) keeps the first one.
1450
+ # @param wait [Numeric, nil] nil: as above
1451
+ def mark_leaving(wait: nil)
1452
+ return if @marked_leaving
1453
+
1454
+ @marked_leaving = true
1455
+ wait ||= @exit_restart ? 0 : @engine.recap_timeout.to_f
1456
+ @owner_lock&.mark_leaving(wait: wait)
1443
1457
  rescue StandardError => e
1444
1458
  Log.warn(:worker, "mark_leaving_failed", error: e.class.name, msg: e.message)
1445
1459
  end
@@ -112,6 +112,7 @@ module Samagotchi
112
112
  module_function
113
113
 
114
114
  def not_live(session_id) = "no live worker for session #{session_id}"
115
+ def unanswered(session_id) = "the worker for session #{session_id} didn't answer"
115
116
 
116
117
  # @param session_dir [String]
117
118
  # @param unlink [Boolean] remove a Stale sidecar (the clients that would
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.121.1
4
+ version: 0.122.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -476,6 +476,7 @@ files:
476
476
  - lib/samagotchi/parent_answers_command.rb
477
477
  - lib/samagotchi/parent_report.rb
478
478
  - lib/samagotchi/participant_messages.rb
479
+ - lib/samagotchi/participant_mode.rb
479
480
  - lib/samagotchi/participant_name.rb
480
481
  - lib/samagotchi/participants.rb
481
482
  - lib/samagotchi/participants_watch.rb
@@ -665,6 +666,8 @@ files:
665
666
  - lib/samagotchi/toolkit.rb
666
667
  - lib/samagotchi/toolkit_binding.rb
667
668
  - lib/samagotchi/toolkit_command.rb
669
+ - lib/samagotchi/toolkit_report_command.rb
670
+ - lib/samagotchi/toolkit_suggest.rb
668
671
  - lib/samagotchi/tools/args.rb
669
672
  - lib/samagotchi/tools/ask_user_question.rb
670
673
  - lib/samagotchi/tools/builtin_calls.rb