samagotchi 0.153.0 → 0.154.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundles.md +9 -12
- data/docs/cli.md +82 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +21 -8
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +4 -2
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +31 -6
- data/lib/samagotchi/answer_text.rb +33 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +52 -32
- data/lib/samagotchi/bridge_client.rb +19 -14
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +3 -3
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +62 -2646
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +7 -6
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -13
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +67 -49
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +27 -7
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +13 -2
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +154 -145
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -11
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +17 -22
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +2 -12
- data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +15 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +11 -7
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -51
- data/lib/samagotchi/web/message_display.rb +7 -0
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +4 -3
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +13 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +6 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +103 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- data/lib/samagotchi.rb +0 -1
- metadata +31 -7
- data/lib/samagotchi/pending_input_queue.rb +0 -55
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -6,6 +6,7 @@ require_relative "../turn_origin"
|
|
|
6
6
|
require_relative "../participant_mode"
|
|
7
7
|
require_relative "../revise_anchor"
|
|
8
8
|
require_relative "../verify_anchor"
|
|
9
|
+
require_relative "../redact"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Web
|
|
@@ -23,23 +24,10 @@ module Samagotchi
|
|
|
23
24
|
# is an allowlist that builds a new hash from named fields, so a field
|
|
24
25
|
# added to an event later never reaches a participant until it is
|
|
25
26
|
# named here, and an event type not named is dropped. Every string
|
|
26
|
-
# that goes out passes Redact (secret shapes, the owner's token, the
|
|
27
|
+
# that goes out passes Redact (lib/samagotchi/redact.rb: secret shapes, the owner's token, the
|
|
27
28
|
# home folder), defence in depth only: the allowlist is the control.
|
|
28
29
|
# Pure: no I/O; keys of what comes in may be strings (JSON) or symbols.
|
|
29
30
|
module ParticipantView
|
|
30
|
-
# Who is looking: +author+ the participant's ("p:<id>"; nil for the
|
|
31
|
-
# owner through the participant view: nothing is theirs), +cwd+ the
|
|
32
|
-
# session's folder (tool paths are shown relative to it, only inside
|
|
33
|
-
# it), +home+ the folder shown as ~, +secrets+ literal strings never
|
|
34
|
-
# shown (the owner's token).
|
|
35
|
-
Viewer = Data.define(:author, :cwd, :home, :secrets) do
|
|
36
|
-
def initialize(author: nil, cwd: nil, home: Dir.home, secrets: [])
|
|
37
|
-
super(author: author, cwd: cwd, home: home, secrets: Array(secrets).map(&:to_s).reject(&:empty?))
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def own?(author) = !self.author.nil? && author.to_s == self.author
|
|
41
|
-
end
|
|
42
|
-
|
|
43
31
|
# A turn's participant_mode values the view passes on (ParticipantMode).
|
|
44
32
|
PARTICIPANT_MODES = [ParticipantMode::REVIEW.to_s, ParticipantMode::PAIR.to_s].freeze
|
|
45
33
|
|
|
@@ -431,74 +419,6 @@ module Samagotchi
|
|
|
431
419
|
|
|
432
420
|
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
433
421
|
end
|
|
434
|
-
|
|
435
|
-
# Secret shapes and host paths out of what a participant gets (the
|
|
436
|
-
# session's folder as relative paths, home as ~). Defence in depth: the
|
|
437
|
-
# allowlist above is the control; this catches a key the model quoted
|
|
438
|
-
# in a reply, or one in a prompt.
|
|
439
|
-
module Redact
|
|
440
|
-
MARK = "[redacted]"
|
|
441
|
-
PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|\z)/m
|
|
442
|
-
PATTERNS = [
|
|
443
|
-
PRIVATE_KEY,
|
|
444
|
-
/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/,
|
|
445
|
-
/\bgh[pousr]_[A-Za-z0-9]{20,}/,
|
|
446
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}/,
|
|
447
|
-
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
448
|
-
/\bxox[abprs]-[A-Za-z0-9-]{10,}/,
|
|
449
|
-
/\bchp_[A-Za-z0-9_-]{20,}/,
|
|
450
|
-
# A one-time link code (LinkCodes::CODE_RE): a bearer credential
|
|
451
|
-
# until its grant expires (an owner's, 10 minutes).
|
|
452
|
-
/\bchc_[A-Za-z0-9_-]{43}/,
|
|
453
|
-
/\bAIza[0-9A-Za-z_-]{30,}/,
|
|
454
|
-
/\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/,
|
|
455
|
-
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
|
|
456
|
-
%r{https://hooks\.slack\.com/services/[A-Za-z0-9/_-]+}
|
|
457
|
-
].freeze
|
|
458
|
-
# The value after an Authorization (or a Bridge's secret,
|
|
459
|
-
# BridgeSecret::HEADER) header or a token=/key= pair.
|
|
460
|
-
VALUES = [
|
|
461
|
-
/(\b(?:Authorization|X-Chi-Bridge):\s*(?:Bearer\s+|Basic\s+|Token\s+)?)[^\s"'`]+/i,
|
|
462
|
-
/((?<![A-Za-z0-9])[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*[=:]\s*["']?)[^\s"'&`,;}]+/i,
|
|
463
|
-
# A URL's password: scheme://user:PASSWORD@host.
|
|
464
|
-
%r{(\b[a-z][a-z0-9+.-]*://[^/\s:@]+:)[^@\s/]+(?=@)}i
|
|
465
|
-
].freeze
|
|
466
|
-
# A text that ends where one of VALUES' values (or a private key's
|
|
467
|
-
# BEGIN line) would go on: a stream cut there would send the value
|
|
468
|
-
# on its own, unredacted (ParticipantStreamFilter never cuts there).
|
|
469
|
-
OPEN_VALUE = /(?:\b(?:Authorization|X-Chi-Bridge):\s*(?:(?:Bearer|Basic|Token)\s*)?|[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*(?:[=:]\s*["']?)?|-----BEGIN [A-Z0-9 ]*)\z/i
|
|
470
|
-
|
|
471
|
-
module_function
|
|
472
|
-
|
|
473
|
-
# +value+ with every string in it redacted (hash values, array items).
|
|
474
|
-
def deep(value, viewer)
|
|
475
|
-
case value
|
|
476
|
-
when String then text(value, viewer)
|
|
477
|
-
when Hash then value.transform_values { |item| deep(item, viewer) }
|
|
478
|
-
when Array then value.map { |item| deep(item, viewer) }
|
|
479
|
-
else value
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def text(value, viewer)
|
|
484
|
-
out = value.dup
|
|
485
|
-
viewer.secrets.each { |secret| out = out.gsub(secret, MARK) }
|
|
486
|
-
PATTERNS.each { |pattern| out = out.gsub(pattern, MARK) }
|
|
487
|
-
VALUES.each { |pattern| out = out.gsub(pattern) { "#{Regexp.last_match(1)}#{MARK}" } }
|
|
488
|
-
out = within(out, viewer.cwd.to_s)
|
|
489
|
-
home = viewer.home.to_s
|
|
490
|
-
out = out.gsub(%r{#{Regexp.escape(home)}(?=/|\b|\z)}, "~") if home.length > 1
|
|
491
|
-
out
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# +text+ with +dir+ (the session's folder) shown relative: "dir/x"
|
|
495
|
-
# as "x", "dir" alone as ".".
|
|
496
|
-
def within(text, dir)
|
|
497
|
-
return text if dir.length < 2
|
|
498
|
-
|
|
499
|
-
text.gsub(%r{#{Regexp.escape(dir)}(/|(?=\b|\z))}) { Regexp.last_match(1) == "/" ? "" : "." }
|
|
500
|
-
end
|
|
501
|
-
end
|
|
502
422
|
end
|
|
503
423
|
end
|
|
504
424
|
end
|
|
@@ -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
|
|
301
|
-
toggle
|
|
302
|
-
|
|
303
|
-
|
|
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"))
|
|
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-
|
|
14
|
-
.
|
|
15
|
-
.block-
|
|
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}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Where a snapshot's card or notice goes by its stamp: the id of the turn
|
|
2
|
+
// opening it comes after (+opening_id+, Bridge::CardPlaces#opening_of: a
|
|
3
|
+
// prompt's turn_id, a wake note's, /continue no's reason line's own id). The
|
|
4
|
+
// page finds that opening's bubble (data-turn-id on a prompt, data-opening-id
|
|
5
|
+
// on a wake note or a reason line, turn_html.js) and the entry goes before
|
|
6
|
+
// the next turn's opening, or at the end. No counting, so the page and the
|
|
7
|
+
// worker can't disagree on which bubbles open turns. An entry with no stamp
|
|
8
|
+
// (an older worker's, one before every opening), or whose opening the page
|
|
9
|
+
// doesn't draw, falls back to its count (card.js cardPlace, turns_since).
|
|
10
|
+
// A new module: the static JS is unversioned (docs/internals/web-frontend.md).
|
|
11
|
+
import { cardPlace } from "./card.js";
|
|
12
|
+
|
|
13
|
+
// The opening bubble +id+ names among the turns' openings (oldest first,
|
|
14
|
+
// bubbles.js turnPromptBubbles), or -1.
|
|
15
|
+
export function stampedOpening(userBubbles, id) {
|
|
16
|
+
if (!id) return -1;
|
|
17
|
+
const stamp = String(id);
|
|
18
|
+
return userBubbles.findIndex((el) => el.dataset?.turnId === stamp || el.dataset?.openingId === stamp);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The element +entry+ goes before: the opening after its stamped one (null:
|
|
22
|
+
// the end), else what its count says (null: the end too).
|
|
23
|
+
export function snapshotPlace(userBubbles, entry) {
|
|
24
|
+
const at = stampedOpening(userBubbles, entry?.opening_id);
|
|
25
|
+
if (at < 0) return cardPlace(userBubbles, entry);
|
|
26
|
+
return userBubbles[at + 1] || null;
|
|
27
|
+
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// above the first turn after them, a finished turn's notice in its step, a
|
|
9
9
|
// finished turn's question resolved where it was asked.
|
|
10
10
|
import { getSession, sendCommand } from "./data.js";
|
|
11
|
-
import {
|
|
11
|
+
import { drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
|
|
12
|
+
import { snapshotPlace } from "./card_stamps.js";
|
|
12
13
|
import { elementOf } from "./turn_html.js";
|
|
13
14
|
import { stepText } from "./turn_block.js";
|
|
14
15
|
import { llmContextNotice, noticeLine } from "./turn_events.js";
|
|
@@ -83,14 +84,15 @@ export function createCardsPlace({
|
|
|
83
84
|
bubbles.placeFollowing(el, before);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// A snapshot's cards and notices, each above the first turn after it
|
|
87
|
+
// A snapshot's cards and notices, each above the first turn after it (the
|
|
88
|
+
// one after its stamped opening, card_stamps.js; else by its count); a
|
|
87
89
|
// turn's own notice where it showed live (placeTurnNotice). +end+: what
|
|
88
90
|
// closes the history below them (a failed turn's prompt), for those with
|
|
89
91
|
// no turn after them; one shown after that prompt went back stays below it.
|
|
90
92
|
function placeSnapshotCards(entries, { before: end = null } = {}) {
|
|
91
93
|
const users = turnPromptBubbles(historyEl);
|
|
92
94
|
for (const entry of entries) {
|
|
93
|
-
const before =
|
|
95
|
+
const before = snapshotPlace(users, entry) || (entry.after_failed ? null : end);
|
|
94
96
|
if (isCard(entry)) upsertCard(entry, { before });
|
|
95
97
|
else if (entry.type === "question") placeResolvedQuestion(entry, before);
|
|
96
98
|
else if (entry.type === "guardrail_warning") bubbles.showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
|
|
@@ -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
|
|
309
|
-
//
|
|
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 (
|
|
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
|
-
//
|
|
45
|
-
// (lib/samagotchi/
|
|
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
|
-
// (
|
|
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 ·
|
|
54
|
+
placeholder: pick("A terminal chi owns this session · a one-turn chi --non-interactive run or an older chi's terminal", "A terminal chi owns this session"),
|
|
55
55
|
hint: "", button: "Send", disabled: true,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -97,13 +97,14 @@ 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 (
|
|
100
|
+
// a worker (every UI can send), or a terminal chi: a one-turn --non-interactive
|
|
101
|
+
// run or an older chi's terminal, in OwnerLock::TUI_BUSY's words.
|
|
101
102
|
export function ownerBadge(owner, id) {
|
|
102
103
|
if (owner === "worker") {
|
|
103
104
|
return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
|
|
104
105
|
}
|
|
105
106
|
if (owner === "tui") {
|
|
106
|
-
return { kind: "tui", text: "terminal", title: "
|
|
107
|
+
return { kind: "tui", text: "terminal", title: "In use by a one-turn chi --non-interactive run or an older chi's terminal" };
|
|
107
108
|
}
|
|
108
109
|
return null;
|
|
109
110
|
}
|
|
@@ -171,7 +172,7 @@ export function childrenSummary(parent, sessions) {
|
|
|
171
172
|
|
|
172
173
|
// Whether the info bar offers "stop": only while a worker runs the session
|
|
173
174
|
// (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
|
|
175
|
+
// stopped or idle-exited session has nothing to stop, and a one-turn run's is
|
|
175
176
|
// refused by the server.
|
|
176
177
|
export function canStopSession({ owner, streaming }) {
|
|
177
178
|
if (owner === "tui") return false;
|
|
@@ -285,9 +285,11 @@ export function noteBubbleTemplate(note) {
|
|
|
285
285
|
if (note?.objectives) return objectivesEntryTemplate(note, timeTitle(note.at));
|
|
286
286
|
const from = note?.label ? `note from ${note.label}` : "note";
|
|
287
287
|
// A wake turn's note opens that turn (TURN_PROMPT, bubbles.js); a class, not
|
|
288
|
-
// data-turn-id, which marks a claim's anchor (claim_link.js).
|
|
288
|
+
// data-turn-id, which marks a claim's anchor (claim_link.js). Its turn's id
|
|
289
|
+
// is its data-opening-id: a card's stamp finds it (card_stamps.js).
|
|
289
290
|
const cls = note?.turn_start ? "bubble note turn-start" : "bubble note";
|
|
290
|
-
|
|
291
|
+
const opening = note?.turn_start && note?.turn_id ? String(note.turn_id) : nothing;
|
|
292
|
+
return html`<div class=${cls} title=${timeTitle(note?.at)} data-opening-id=${opening}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
// A plugin's steer (a nudge put into the running turn): one line that
|
|
@@ -311,7 +313,9 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
|
|
|
311
313
|
// participant's badge (participant_names.js authorBadge: their name and
|
|
312
314
|
// colour) in place of +label+. A turn's opening carries its Revise stamp
|
|
313
315
|
// (+message.revises+: data-revises, revise_stack.js) and whether it was a
|
|
314
|
-
// Verify turn (+message.verify+: data-verify, no Revise on its answer).
|
|
316
|
+
// Verify turn (+message.verify+: data-verify, no Revise on its answer). An
|
|
317
|
+
// opening no turn ran (/continue no's reason) has +message.opening_id+
|
|
318
|
+
// instead of a turn id: data-opening-id, a card's stamp (card_stamps.js).
|
|
315
319
|
// +message.source+ (a delegate report's "delegate_report") is its
|
|
316
320
|
// data-source; +lineKey+, a merged line's fold key (answer_fold.js:
|
|
317
321
|
// data-line-key, the reload's "<turn id>:input:<place>").
|
|
@@ -320,7 +324,7 @@ export function userBubbleTemplate(message, { label = null, state = null, step =
|
|
|
320
324
|
const from = author
|
|
321
325
|
? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name, author.mode)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name, author.mode)}</span>`
|
|
322
326
|
: label ? html`<span class="origin-label">${label}</span>` : nothing;
|
|
323
|
-
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-participant-mode=${author?.mode || 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-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
|
|
327
|
+
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-participant-mode=${author?.mode || 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-opening-id=${(!message?.turn_id && message?.opening_id) || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
// A user line's text: plain (its quotes set apart), or the server's
|
|
@@ -7,7 +7,7 @@ require "openssl"
|
|
|
7
7
|
require "time"
|
|
8
8
|
require "uri"
|
|
9
9
|
require_relative "crypto"
|
|
10
|
-
require_relative "
|
|
10
|
+
require_relative "../../private_files"
|
|
11
11
|
require_relative "../../log"
|
|
12
12
|
require_relative "../../atomic_file"
|
|
13
13
|
require_relative "../../session"
|
|
@@ -54,6 +54,14 @@ module Samagotchi
|
|
|
54
54
|
json_response(status, { error: code, detail: detail, message: error.message, **fields }.compact)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# 409 scratch for a chi scratch session +id+, which only its terminal
|
|
58
|
+
# uses (Session::SCRATCH_REFUSED): the session page's read and stream
|
|
59
|
+
# (a tab opened at its id by hand: the hub never lists it), as
|
|
60
|
+
# POST …/wake refuses it.
|
|
61
|
+
def scratch_refused(id)
|
|
62
|
+
refusal_response(409, "scratch", SessionManager::ScratchSession.new(id), session_id: id)
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
# The body as a Hash ({} when there is none), or nil when it isn't one.
|
|
58
66
|
def body_hash(req)
|
|
59
67
|
raw = request_body(req)
|
|
@@ -256,7 +264,7 @@ module Samagotchi
|
|
|
256
264
|
# status row), parent_strategy, installed (the newest chi here)}. A
|
|
257
265
|
# worker still leaving is waited for first (Local#wake: its recap's
|
|
258
266
|
# timeout and a margin, on top of +wait+); 409
|
|
259
|
-
# owned_by_tui (a chi
|
|
267
|
+
# owned_by_tui (a one-turn chi run has it), 409 scratch (a leftover scratch
|
|
260
268
|
# session, as chi --attach here says); 504 worker_timeout (its Bridge
|
|
261
269
|
# didn't come in time); 409 worker_newer (a worker whose protocol is newer
|
|
262
270
|
# than this chi's, not resumed); 404 not_found.
|
|
@@ -425,7 +433,7 @@ module Samagotchi
|
|
|
425
433
|
# session (NoteDelivery, as chi note; source: where it came from,
|
|
426
434
|
# "cli" by default, never "broadcast"). 200 {status: "queued" (a
|
|
427
435
|
# worker adds it) or "waits" (the next start does), session_id,
|
|
428
|
-
# queued}; 409 owned_by_tui (status refused: a chi
|
|
436
|
+
# queued}; 409 owned_by_tui (status refused: a one-turn chi run has it); 400
|
|
429
437
|
# note_rejected (empty, or over 16 KiB).
|
|
430
438
|
def handle_notes(req, id)
|
|
431
439
|
body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
|
|
@@ -438,8 +446,9 @@ module Samagotchi
|
|
|
438
446
|
session_access.show(id)
|
|
439
447
|
result = session_access.note(id, text: text, source: source.strip)
|
|
440
448
|
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
|
|
449
|
+
error = result.scratch ? SessionManager::ScratchSession.new(id) : SessionManager::OwnedByTUI.new(id)
|
|
450
|
+
return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0,
|
|
451
|
+
scratch: result.scratch || nil)
|
|
443
452
|
end
|
|
444
453
|
|
|
445
454
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
@@ -40,6 +40,10 @@ module Samagotchi
|
|
|
40
40
|
return handle_brief_read(id) if req.params["brief"] == "1"
|
|
41
41
|
|
|
42
42
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
43
|
+
# A tab opened at a chi scratch session's id: its terminal's alone.
|
|
44
|
+
# (The light re-reads come after a full read only.)
|
|
45
|
+
return scratch_refused(id) if session.scratch
|
|
46
|
+
|
|
43
47
|
# Read-only preview: selecting a session never spawns a worker.
|
|
44
48
|
# The worker is woken only on POST /turn (handle_turn) via
|
|
45
49
|
# SessionManager.resume_session + write_turn_input. This avoids
|
|
@@ -120,8 +124,8 @@ module Samagotchi
|
|
|
120
124
|
end
|
|
121
125
|
|
|
122
126
|
# 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
|
|
124
|
-
# REPL's (
|
|
127
|
+
# dead worker saved can't be answered (its turn is gone), nor a
|
|
128
|
+
# one-shot's or an older chi's REPL's (they share nothing with the web).
|
|
125
129
|
# A question between turns (the step-limit one) is the snapshot's own.
|
|
126
130
|
# Its refs (the asking reply's, QuestionAsked) are checked again.
|
|
127
131
|
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
|
|
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,
|