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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -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 +80 -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 +3 -3
- 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 +1 -1
- data/docs/internals/remote-sessions.md +7 -7
- 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 +16 -6
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- 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.rb +2 -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/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 +2 -2
- 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/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 +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- 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/pending_input_queue.rb +2 -2
- 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 +59 -36
- 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 +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- 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 +2 -3
- 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 +1 -7
- 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 +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -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 +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- 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/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 +3 -3
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -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 +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +100 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +29 -6
- 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
|
@@ -11,8 +11,7 @@ module Samagotchi
|
|
|
11
11
|
# Engine#run_turn) as [SYSTEM: REMINDERS DUE <local time>] and marks it fired.
|
|
12
12
|
# 2. Synthetic turns: when this job's tick finds due reminders while
|
|
13
13
|
# idle, it fires @auto_turn_callback, which queues their names
|
|
14
|
-
# (Engine#note_due_reminders) for the worker's
|
|
15
|
-
# reminder turn. While names are queued the tick waits; whoever
|
|
14
|
+
# (Engine#note_due_reminders) for the worker's reminder turn. While names are queued the tick waits; whoever
|
|
16
15
|
# clears them (a turn's injection, the reminder turn's consumer)
|
|
17
16
|
# re-arms it. The queue is the only "already due" state.
|
|
18
17
|
class IdleReminders
|
|
@@ -64,8 +63,8 @@ module Samagotchi
|
|
|
64
63
|
|
|
65
64
|
# Signal the engine to create a synthetic turn via callback.
|
|
66
65
|
# The callback is responsible for triggering a turn (e.g. SessionManager
|
|
67
|
-
# writes a file
|
|
68
|
-
#
|
|
66
|
+
# writes a file). The engine's run_turn then picks up and delivers
|
|
67
|
+
# the reminders.
|
|
69
68
|
return unless @auto_turn_callback
|
|
70
69
|
|
|
71
70
|
@auto_turn_callback.call(due_names)
|
|
@@ -5,8 +5,8 @@ require_relative "log"
|
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
7
|
# A turn's iteration limit (how many model ↔ tool rounds before the turn
|
|
8
|
-
# stops and offers to continue), one resolver for the Engine
|
|
9
|
-
#
|
|
8
|
+
# stops and offers to continue), one resolver for the Engine and the
|
|
9
|
+
# worker.
|
|
10
10
|
module IterationLimit
|
|
11
11
|
KEY = "turn.max_iterations"
|
|
12
12
|
# A turn's limit unless turn.max_iterations says otherwise, and the
|
|
@@ -11,6 +11,7 @@ require_relative "prompt_literal_guard"
|
|
|
11
11
|
require_relative "token_usage"
|
|
12
12
|
require_relative "client"
|
|
13
13
|
require_relative "llm/errors"
|
|
14
|
+
require_relative "degrade"
|
|
14
15
|
require_relative "log"
|
|
15
16
|
require_relative "empty_answer_retry"
|
|
16
17
|
require_relative "cut"
|
|
@@ -198,9 +199,9 @@ module Samagotchi
|
|
|
198
199
|
# memory_read's overflow guard. nil when it can't be read.
|
|
199
200
|
# @return [ContextWindow::Resolved, nil]
|
|
200
201
|
def tool_output_window
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
Degrade.call(:model, "context_window_unresolved") do
|
|
203
|
+
ContextWindow.resolve(client: @client, model: @turn_settings&.model_name, setting: @turn_settings&.window_setting)
|
|
204
|
+
end
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
def guard_generation
|
|
@@ -383,9 +384,9 @@ module Samagotchi
|
|
|
383
384
|
end
|
|
384
385
|
|
|
385
386
|
def model_props
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
Degrade.call(:model, "server_props_unreadable") do
|
|
388
|
+
@client.respond_to?(:server_props) ? @client.server_props(model: @turn_settings.model_name) : nil
|
|
389
|
+
end
|
|
389
390
|
end
|
|
390
391
|
|
|
391
392
|
# One iteration. Returns :next, :answer (the turn ends), or the turn's
|
|
@@ -831,10 +832,9 @@ module Samagotchi
|
|
|
831
832
|
cancel_controller: turn.cancel_controller, answer: answer)
|
|
832
833
|
end
|
|
833
834
|
|
|
835
|
+
# A failing stream callback (a UI gone) must not break the turn.
|
|
834
836
|
def emit_stream_event(callback, event)
|
|
835
|
-
callback&.call(event)
|
|
836
|
-
rescue StandardError
|
|
837
|
-
nil
|
|
837
|
+
Degrade.call(:turn, "stream_event_callback_failed", type: (event[:type] if event.is_a?(Hash))) { callback&.call(event) }
|
|
838
838
|
end
|
|
839
839
|
|
|
840
840
|
# ── Hook dispatch helper ───────────────────────────────────────────────────
|
|
@@ -849,9 +849,8 @@ module Samagotchi
|
|
|
849
849
|
def fire_hook(name, event)
|
|
850
850
|
return unless @hooks
|
|
851
851
|
|
|
852
|
-
@hooks.fire(name, event)
|
|
853
|
-
rescue StandardError
|
|
854
852
|
# A failing hook must not break the turn.
|
|
853
|
+
Degrade.call(:hooks, "hook_fire_failed", hook: name) { @hooks.fire(name, event) }
|
|
855
854
|
end
|
|
856
855
|
|
|
857
856
|
# Coerce a value to boolean — handles true/false, nil, and string "true"/"false".
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
-
# How `bin/chi` runs a session: attached to a worker (
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# can't do yet.
|
|
4
|
+
# How `bin/chi` runs a session: attached to a worker (the attached TUI,
|
|
5
|
+
# AttachLauncher), or --non-interactive's one turn in this process
|
|
6
|
+
# (TerminalUI, the one-shot host).
|
|
8
7
|
module LaunchMode
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
# reach the worker: a /model command, and no_interrupt on each posted
|
|
12
|
-
# turn; --memory and --mute are session fields the worker reads.)
|
|
13
|
-
REPL_ONLY = { verbose: "--verbose" }.freeze
|
|
14
|
-
# What an attached run can't be combined with (the worker runs its turns).
|
|
15
|
-
ATTACHED_CONFLICTS = { non_interactive: "--non-interactive", verbose: "--verbose" }.freeze
|
|
16
|
-
# Opening a `chi scratch` session a killed REPL left behind would turn it
|
|
17
|
-
# into a kept one.
|
|
18
|
-
SCRATCH_LEFTOVER = "that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
|
|
8
|
+
# What an attached run can't be combined with.
|
|
9
|
+
ATTACHED_CONFLICTS = { non_interactive: "--non-interactive" }.freeze
|
|
19
10
|
# A worker spawned or resumed for an attach whose Bridge never came (%s:
|
|
20
11
|
# the session id). A worker whose chi home couldn't load chi logs
|
|
21
12
|
# SessionManager::HOME_BOOT_FAILED and exits: HomeDoctor's boot row says
|
|
@@ -30,20 +21,12 @@ module Samagotchi
|
|
|
30
21
|
module_function
|
|
31
22
|
|
|
32
23
|
# @param options [Hash] bin/chi's parsed options
|
|
33
|
-
# @
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return [:attached, nil] if options[:attach] || options[:shared]
|
|
38
|
-
# --no-shared, or nobody asked for attached mode.
|
|
39
|
-
return [:repl, nil] if options[:shared] == false || !shared_config
|
|
40
|
-
# A one-shot with no REPL: nothing to attach.
|
|
41
|
-
return [:repl, nil] if options[:non_interactive]
|
|
24
|
+
# @return [Symbol] :attached, or :one_shot (--non-interactive: one turn
|
|
25
|
+
# in this process)
|
|
26
|
+
def resolve(options)
|
|
27
|
+
return :attached if options[:attach]
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
return [:repl, "(session.shared: #{flag} runs in a plain REPL)"] if flag
|
|
45
|
-
|
|
46
|
-
[:attached, nil]
|
|
29
|
+
options[:non_interactive] ? :one_shot : :attached
|
|
47
30
|
end
|
|
48
31
|
|
|
49
32
|
# Why an attached run's flags don't go together, or nil when they do.
|
|
@@ -51,12 +34,10 @@ module Samagotchi
|
|
|
51
34
|
# @return [String, nil]
|
|
52
35
|
def attached_conflict(options)
|
|
53
36
|
conflict = ATTACHED_CONFLICTS.find { |key, _flag| options[key] }
|
|
54
|
-
if options[:attach] && options[:
|
|
55
|
-
"use either --attach ID or --
|
|
56
|
-
elsif options[:attach] && options[:resume]
|
|
57
|
-
"--attach takes the session id; --resume goes with --shared"
|
|
37
|
+
if options[:attach] && options[:resume]
|
|
38
|
+
"use either --attach ID or --resume ID"
|
|
58
39
|
elsif conflict
|
|
59
|
-
"--attach
|
|
40
|
+
"--attach can't be combined with #{conflict.last}"
|
|
60
41
|
end
|
|
61
42
|
end
|
|
62
43
|
end
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "cache_confirm"
|
|
4
|
+
require_relative "session"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
# A command for a session's own level of a setting (/thinking,
|
|
7
8
|
# /reasoning-history): with no argument, the level the next turn runs at
|
|
8
9
|
# and where it came from; with a level, sets the session's own (the
|
|
9
10
|
# setting's unset word unsets it). A change takes effect at the next
|
|
10
|
-
# turn's start (the
|
|
11
|
+
# turn's start (the worker runs it between turns), and the
|
|
11
12
|
# reply says what it costs the prompt cache. A costly change asks first
|
|
12
13
|
# (CacheConfirm); --yes (or -y) applies it.
|
|
13
14
|
#
|
|
@@ -47,7 +48,7 @@ module Samagotchi
|
|
|
47
48
|
known, own = spec.parse.call(word)
|
|
48
49
|
raise ArgumentError, "unknown #{spec.label} level #{word}\n#{spec.usage}" unless known
|
|
49
50
|
|
|
50
|
-
session = @engine.session or raise ArgumentError,
|
|
51
|
+
session = @engine.session or raise ArgumentError, Session::NONE_YET
|
|
51
52
|
before = explained
|
|
52
53
|
ask = confirm && !yes ? ask(before, own, word) : nil
|
|
53
54
|
return ask if ask
|
|
@@ -5,6 +5,7 @@ require_relative "llm_context_strategy"
|
|
|
5
5
|
require_relative "llm_context_edit"
|
|
6
6
|
require_relative "llm_context_stale"
|
|
7
7
|
require_relative "cache_confirm"
|
|
8
|
+
require_relative "session"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
10
11
|
# /llm-context, the session's own LLM context values
|
|
@@ -14,7 +15,7 @@ module Samagotchi
|
|
|
14
15
|
# `strategy <layers|none>`, `apply <rule>` and `budget <N|off>` (any of
|
|
15
16
|
# them, in one line; `default` unsets one) it sets the session's own, and
|
|
16
17
|
# `reset` unsets them all. A change takes effect at the next turn's
|
|
17
|
-
# start (the
|
|
18
|
+
# start (the worker runs it between turns), and the reply
|
|
18
19
|
# says what it costs: forget on or off changes the tool list and the
|
|
19
20
|
# system prompt (a full cache break); stale turned on stages the reads
|
|
20
21
|
# already superseded as one batch under the apply rule; a layer turned
|
|
@@ -49,7 +50,7 @@ module Samagotchi
|
|
|
49
50
|
args, yes = CacheConfirm.split(args, downcase: false)
|
|
50
51
|
return [listing(@engine.llm_context_explained), false, nil] if args.empty?
|
|
51
52
|
|
|
52
|
-
session = @engine.session or raise ArgumentError,
|
|
53
|
+
session = @engine.session or raise ArgumentError, Session::NONE_YET
|
|
53
54
|
before = @engine.llm_context_explained
|
|
54
55
|
updated = args.casecmp?(RESET) ? LLMContextOverride.new : LLMContextOverride.update(@engine.llm_context_override, words(args))
|
|
55
56
|
ask = confirm && !yes ? ask(session, before, updated, args) : nil
|
data/lib/samagotchi/log.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Samagotchi
|
|
|
71
71
|
@monitor.synchronize { close_state }
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
# The session this process works for (a worker,
|
|
74
|
+
# The session this process works for (a worker, a one-shot); records
|
|
75
75
|
# carry its first SID_LENGTH chars unless they pass their own sid:.
|
|
76
76
|
attr_accessor :session_id
|
|
77
77
|
|
|
@@ -127,7 +127,7 @@ module Samagotchi
|
|
|
127
127
|
state[:writer].write(text)
|
|
128
128
|
mirror(text) if state[:mirror] && !echo
|
|
129
129
|
text
|
|
130
|
-
rescue StandardError
|
|
130
|
+
rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
131
131
|
nil
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -152,7 +152,7 @@ module Samagotchi
|
|
|
152
152
|
ts: Time.now, level: level.to_s.upcase, tag: tag, pid: Process.pid,
|
|
153
153
|
sid: sid && sid.to_s[0, SID_LENGTH], event: event, fields: fields, payload: payload
|
|
154
154
|
))
|
|
155
|
-
rescue StandardError
|
|
155
|
+
rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
156
156
|
nil
|
|
157
157
|
end
|
|
158
158
|
|
|
@@ -170,13 +170,13 @@ module Samagotchi
|
|
|
170
170
|
|
|
171
171
|
def echo_to_stderr(text)
|
|
172
172
|
$stderr.puts(text)
|
|
173
|
-
rescue StandardError
|
|
173
|
+
rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
174
174
|
nil
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
def mirror(text)
|
|
178
178
|
$stderr.write(text)
|
|
179
|
-
rescue StandardError
|
|
179
|
+
rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
180
180
|
nil
|
|
181
181
|
end
|
|
182
182
|
|
|
@@ -201,8 +201,8 @@ module Samagotchi
|
|
|
201
201
|
def build_state
|
|
202
202
|
options = @options
|
|
203
203
|
path = options.fetch(:path, :auto)
|
|
204
|
-
path = LogPath.resolve rescue nil if path == :auto
|
|
205
|
-
level = options[:level] || (Config.get("log.level") rescue nil)
|
|
204
|
+
path = LogPath.resolve rescue nil if path == :auto # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
205
|
+
level = options[:level] || (Config.get("log.level") rescue nil) # degrade-ok: Log's own internals: a failure here must not recurse into Log
|
|
206
206
|
level = LEVELS.key?(level.to_s.downcase.to_sym) ? level.to_s.downcase.to_sym : DEFAULT_LEVEL
|
|
207
207
|
{ writer: DebugLog.new(path: path), level: LEVELS.fetch(level), mirror: options[:mirror] ? true : false }
|
|
208
208
|
end
|
data/lib/samagotchi/log_line.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Samagotchi
|
|
|
16
16
|
# (debug dumps) are indented by four spaces and belong to the record above.
|
|
17
17
|
module LogLine
|
|
18
18
|
LEVELS = %w[DEBUG INFO WARN ERROR].freeze
|
|
19
|
-
TAGS = %w[turn http worker bridge web attached
|
|
19
|
+
TAGS = %w[turn http worker bridge web attached idle recap hooks plugins guardrails config memory model context broadcast toolkit system_one prompt].freeze
|
|
20
20
|
INDENT = " "
|
|
21
21
|
|
|
22
22
|
Record = Struct.new(:ts, :level, :tag, :pid, :sid, :event, :fields, :payload, keyword_init: true)
|
data/lib/samagotchi/log_path.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative "config"
|
|
|
4
4
|
require_relative "paths"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
|
-
# Where the debug log (DebugLog) goes, for
|
|
7
|
+
# Where the debug log (DebugLog) goes, for a one-shot, the attached terminal
|
|
8
8
|
# and the background worker alike: nil when log.disable is set, else log.file
|
|
9
9
|
# (~ and relative paths expanded against cwd), else
|
|
10
10
|
# $XDG_STATE_HOME/samagotchi/samagotchi.log (~/.local/state/... by default),
|
|
@@ -123,7 +123,7 @@ module Samagotchi
|
|
|
123
123
|
|
|
124
124
|
def outcome(result)
|
|
125
125
|
case result.status
|
|
126
|
-
when :refused then "refused: #{
|
|
126
|
+
when :refused then "refused: #{result.refusal}"
|
|
127
127
|
when :queued then "queued: its worker adds it within a few seconds"
|
|
128
128
|
else "waits for the session's next start (#{result.queued} #{result.queued == 1 ? "note" : "notes"} queued)"
|
|
129
129
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "session"
|
|
4
|
+
require_relative "owner_lock"
|
|
4
5
|
require_relative "session_inbox"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
@@ -9,21 +10,28 @@ module Samagotchi
|
|
|
9
10
|
autoload :SessionManager, File.expand_path("session_manager", __dir__)
|
|
10
11
|
|
|
11
12
|
# The one rule for putting a context note into a session, shared by
|
|
12
|
-
# `chi note`, the agent's send_note and `chi broadcast`: a session a
|
|
13
|
-
#
|
|
14
|
-
# note in its notes/ folder, which a live worker adds within
|
|
15
|
-
# a session with no worker at its next start. Callers word
|
|
13
|
+
# `chi note`, the agent's send_note and `chi broadcast`: a session a
|
|
14
|
+
# one-turn chi run owns, or a chi scratch session, refuses it; any other
|
|
15
|
+
# gets the note in its notes/ folder, which a live worker adds within
|
|
16
|
+
# seconds and a session with no worker at its next start. Callers word
|
|
17
|
+
# the outcome (#refusal for a refused one).
|
|
16
18
|
module NoteDelivery
|
|
17
|
-
# Why a chi
|
|
18
|
-
|
|
19
|
+
# Why a one-turn chi run's session takes no note.
|
|
20
|
+
ONE_SHOT_REFUSAL = "it is #{OwnerLock::TUI_BUSY}; send the note once it ends"
|
|
19
21
|
|
|
20
22
|
# The outcome for one session.
|
|
21
23
|
# @!attribute status [Symbol] :queued (a worker runs it), :waits (no
|
|
22
|
-
# worker: it waits for the next start) or :refused (a chi
|
|
24
|
+
# worker: it waits for the next start) or :refused (a one-turn chi run
|
|
25
|
+
# owns it, or it is a chi scratch session: +scratch+)
|
|
23
26
|
# @!attribute queued [Integer] notes waiting in its notes/ folder
|
|
24
27
|
# (:waits only; 0 otherwise)
|
|
25
|
-
Result = Data.define(:id, :status, :queued) do
|
|
28
|
+
Result = Data.define(:id, :status, :queued, :scratch) do
|
|
29
|
+
def initialize(id:, status:, queued:, scratch: false) = super
|
|
30
|
+
|
|
26
31
|
def delivered? = status != :refused
|
|
32
|
+
|
|
33
|
+
# Why a refused note was refused, in its condition's one wording.
|
|
34
|
+
def refusal = scratch ? Session::SCRATCH_REFUSED : ONE_SHOT_REFUSAL
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
module_function
|
|
@@ -35,6 +43,8 @@ module Samagotchi
|
|
|
35
43
|
def deliver(id, text:, source:, state_dir:, from_session: nil, from_cwd: nil, objectives: nil)
|
|
36
44
|
owner = SessionManager.session_owner(id, state_dir: state_dir)
|
|
37
45
|
return Result.new(id: id, status: :refused, queued: 0) if owner&.tui?
|
|
46
|
+
# Its terminal's alone, live or left behind.
|
|
47
|
+
return Result.new(id: id, status: :refused, queued: 0, scratch: true) if Session.load(id, state_dir: state_dir).scratch
|
|
38
48
|
|
|
39
49
|
path = SessionInbox.write_note(id, text: text, source: source, from_session: from_session, from_cwd: from_cwd,
|
|
40
50
|
objectives: objectives, state_dir: state_dir)
|
|
@@ -10,6 +10,7 @@ require_relative "llm_context_stale"
|
|
|
10
10
|
require_relative "log"
|
|
11
11
|
require_relative "message_time"
|
|
12
12
|
require_relative "objectives_check"
|
|
13
|
+
require_relative "objectives_edit"
|
|
13
14
|
require_relative "session"
|
|
14
15
|
require_relative "session_chain"
|
|
15
16
|
require_relative "session_inbox"
|
|
@@ -103,13 +104,19 @@ module Samagotchi
|
|
|
103
104
|
new(change: change, id: id, text: kept, note: note, was: Objectives.fingerprint(item))
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
# What's wrong with it against +list+, nil when it can apply
|
|
107
|
+
# What's wrong with it against +list+, nil when it can apply: its #line
|
|
108
|
+
# through the same Edit its card's Apply runs, so what Apply would refuse
|
|
109
|
+
# is refused here.
|
|
107
110
|
def refusal(list)
|
|
108
111
|
return "change must be add, reword, done or drop" unless %w[add reword done drop].include?(change)
|
|
109
112
|
return "#{change} needs a text" if %w[add reword].include?(change) && text.empty?
|
|
110
|
-
return
|
|
113
|
+
return Objectives.unknown(id, list) unless change == "add" || list.find(id)
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
verb, rest = line.delete_prefix("/objectives ").split(/\s+/, 2)
|
|
116
|
+
edited = Edit.apply(list, verb, rest.to_s, was: was)
|
|
117
|
+
return "#{change} needs a text of its own, not only a status or a done-when" if edited.nil?
|
|
118
|
+
|
|
119
|
+
edited.is_a?(String) ? edited : nil
|
|
113
120
|
end
|
|
114
121
|
|
|
115
122
|
# The /objectives line that applies it. An add's text can't split in two.
|
|
@@ -240,6 +247,20 @@ module Samagotchi
|
|
|
240
247
|
AtomicFile.write(path, JSON.generate(known.merge(card => state)))
|
|
241
248
|
end
|
|
242
249
|
|
|
250
|
+
# The one wording per refusal: the command, a proposal, a move and a check
|
|
251
|
+
# say these, each adding only its own context.
|
|
252
|
+
# @param ids [Array<String>, String] the ids the list doesn't have
|
|
253
|
+
def unknown(ids, list)
|
|
254
|
+
known = list.items.map(&:id)
|
|
255
|
+
"no objective #{Array(ids).join(", ")}#{" (this session's: #{known.join(", ")})" unless known.empty?}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @param ids [Array<String>, String] dropped ids
|
|
259
|
+
def dropped(ids)
|
|
260
|
+
ids = Array(ids)
|
|
261
|
+
"#{ids.join(", ")} #{ids.size == 1 ? "is" : "are"} dropped; /objectives undo #{ids.join(" ")} brings #{ids.size == 1 ? "it" : "them"} back"
|
|
262
|
+
end
|
|
263
|
+
|
|
243
264
|
# The id a call names, as the list spells it.
|
|
244
265
|
def move_id(call) = call[:id].to_s.strip.upcase
|
|
245
266
|
|
|
@@ -6,16 +6,14 @@ require "json"
|
|
|
6
6
|
require "securerandom"
|
|
7
7
|
require "time"
|
|
8
8
|
|
|
9
|
+
require_relative "answer_text"
|
|
9
10
|
require_relative "config"
|
|
10
11
|
require_relative "context_note"
|
|
11
12
|
require_relative "llm_context_stale"
|
|
12
13
|
require_relative "log"
|
|
13
|
-
require_relative "
|
|
14
|
-
require_relative "model_profile"
|
|
15
|
-
require_relative "output_formatter"
|
|
14
|
+
require_relative "redact"
|
|
16
15
|
require_relative "session"
|
|
17
16
|
require_relative "steer"
|
|
18
|
-
require_relative "tool_call_parser"
|
|
19
17
|
require_relative "verdict"
|
|
20
18
|
|
|
21
19
|
module Samagotchi
|
|
@@ -51,9 +49,6 @@ module Samagotchi
|
|
|
51
49
|
LEFT_OUT = "[earlier record left out]"
|
|
52
50
|
# What the agent said to the user, in the record.
|
|
53
51
|
OWN_WORDS = "[agent's own words]"
|
|
54
|
-
# A thought cut off before its close: the whole entry, from an opening at its
|
|
55
|
-
# start. MessageParts' forms are the closed one and the template-opened one.
|
|
56
|
-
UNCLOSED_THINK = /\A\s*<think>.*\z/m
|
|
57
52
|
SYSTEM = "You check one claim a coding agent made about its own work, for the user. You see only the claim and the " \
|
|
58
53
|
"session's record: what the user and others wrote, what the tools returned, and what the agent said to the " \
|
|
59
54
|
"user, marked #{OWN_WORDS}. You didn't do the work and you owe the agent nothing. The agent's own words " \
|
|
@@ -77,7 +72,7 @@ module Samagotchi
|
|
|
77
72
|
# @param args [String] the objective's id
|
|
78
73
|
# @return [Array(String, Symbol)] the reply, and :ok or :error
|
|
79
74
|
def run(engine, args)
|
|
80
|
-
session = engine.session or return [
|
|
75
|
+
session = engine.session or return [Session::NONE_YET, :error]
|
|
81
76
|
id = args.to_s.strip.upcase
|
|
82
77
|
return ["usage: /objectives check ID", :error] unless Objectives::ID.match?(id)
|
|
83
78
|
# The running turn's text is theirs: it doesn't go to a side request.
|
|
@@ -98,8 +93,8 @@ module Samagotchi
|
|
|
98
93
|
model = configured_model
|
|
99
94
|
Objectives.with_lock(dir) do
|
|
100
95
|
list = Objectives.current(session.id, session.messages, state_dir: engine.session_state_dir, running: conversation)
|
|
101
|
-
item = list.find(id) or next [
|
|
102
|
-
next [
|
|
96
|
+
item = list.find(id) or next [Objectives.unknown(id, list), :error]
|
|
97
|
+
next [Objectives.dropped(id), :error] if item.status == Objectives::DROPPED
|
|
103
98
|
|
|
104
99
|
move = list.moves[id] or next ["#{id} has no move to check yet", :error]
|
|
105
100
|
name = model || engine.effective_model_ref
|
|
@@ -167,7 +162,7 @@ module Samagotchi
|
|
|
167
162
|
|
|
168
163
|
# The chat request: the objective, the claim and the record, in a frame whose
|
|
169
164
|
# tag no text in them could know, so tool output can't close it. Secrets
|
|
170
|
-
# chi's participant redaction knows (
|
|
165
|
+
# chi's participant redaction knows (Redact, by shape and
|
|
171
166
|
# by name, no model involved) don't leave: objectives.check_model may be
|
|
172
167
|
# another host.
|
|
173
168
|
# @param item [Objectives::Item]
|
|
@@ -182,8 +177,7 @@ module Samagotchi
|
|
|
182
177
|
end
|
|
183
178
|
|
|
184
179
|
def redacted(text)
|
|
185
|
-
|
|
186
|
-
Web::ParticipantView::Redact.text(text, Web::ParticipantView::Viewer.new(cwd: nil))
|
|
180
|
+
Redact.text(text, Redact::Viewer.new(cwd: nil))
|
|
187
181
|
end
|
|
188
182
|
private_class_method :redacted
|
|
189
183
|
|
|
@@ -217,7 +211,7 @@ module Samagotchi
|
|
|
217
211
|
|
|
218
212
|
case (entry[:role] || entry["role"]).to_s
|
|
219
213
|
when "model"
|
|
220
|
-
words =
|
|
214
|
+
words = AnswerText.visible(text)
|
|
221
215
|
"#{OWN_WORDS}\n#{cut(words, OUTPUT_CHARS)}" unless words.empty?
|
|
222
216
|
when "user" then "[#{Steer.participant_message?(entry) ? "participant" : "user"}]\n#{cut(text, OUTPUT_CHARS)}"
|
|
223
217
|
when "system"
|
|
@@ -229,19 +223,6 @@ module Samagotchi
|
|
|
229
223
|
end
|
|
230
224
|
private_class_method :said_part
|
|
231
225
|
|
|
232
|
-
# What the user saw of a model entry. A chat entry's content is that already
|
|
233
|
-
# (its reasoning is the separate :thinking, its calls :tool_calls); a native
|
|
234
|
-
# entry's is the raw response, thoughts and call markup inline.
|
|
235
|
-
# @return [String]
|
|
236
|
-
def own_words(text)
|
|
237
|
-
gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
|
|
238
|
-
text = gemma.strip_thought(text).gsub(MessageParts::QWEN_THINK, "").sub(UNCLOSED_THINK, "")
|
|
239
|
-
.sub(MessageParts::QWEN_OPEN_THINK, "")
|
|
240
|
-
text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
|
|
241
|
-
OutputFormatter.strip_markup(text)
|
|
242
|
-
end
|
|
243
|
-
private_class_method :own_words
|
|
244
|
-
|
|
245
226
|
# A multipart entry's text parts.
|
|
246
227
|
def text_of(content)
|
|
247
228
|
return content.to_s.scrub if content.is_a?(String)
|
|
@@ -8,7 +8,7 @@ module Samagotchi
|
|
|
8
8
|
# /objectives: with no arguments the session's list (Objectives), each
|
|
9
9
|
# objective's latest move under it (the model's objectives tool; a move
|
|
10
10
|
# changes nothing); `set`, `add`, `edit`, `done`, `drop` and `undo` edit it. An edit is a
|
|
11
|
-
# whole new list in a note file (NoteDelivery, with its refusal of a chi
|
|
11
|
+
# whole new list in a note file (NoteDelivery, with its refusal of a one-turn chi run's session), so it is
|
|
12
12
|
# safe from any thread, a turn running or not: it goes into the conversation
|
|
13
13
|
# at the worker's next pass between turns, and a second edit stacks on an
|
|
14
14
|
# unabsorbed first. Nothing here saves the session.
|
|
@@ -18,10 +18,6 @@ module Samagotchi
|
|
|
18
18
|
"(a text may end \"(done when: …)\"; lines work as | does)".freeze
|
|
19
19
|
NONE = "no objectives; #{NAME} set A | B sets them".freeze
|
|
20
20
|
BETWEEN_TURNS = "(an edit goes into the conversation between turns)"
|
|
21
|
-
SEPARATOR = /\n|(?:\A|\s)\|(?:\s|\z)/
|
|
22
|
-
# What the frame shows a status as: typed at a text's start it would pass
|
|
23
|
-
# for one (Objectives::Item#line).
|
|
24
|
-
STATUS_MARK = /\A(?:\[(?:#{Objectives::STATUSES.join("|")})\]\s*)+/i
|
|
25
21
|
# A proposal card's action (Tools::ObjectivesTool): the edit, then the card
|
|
26
22
|
# folds to a line, once: it is recorded, so a repeat applies nothing. A
|
|
27
23
|
# reword's --was says what it was proposed against. The prefix is the
|
|
@@ -30,7 +26,6 @@ module Samagotchi
|
|
|
30
26
|
FOLDS = { Objectives::APPLIED => "Proposal applied", Objectives::DISMISSED => "Proposal dismissed" }.freeze
|
|
31
27
|
DISMISS = "dismiss"
|
|
32
28
|
CHECK = /\Acheck(?:\s|\z)/i
|
|
33
|
-
MARKS = { "done" => Objectives::DONE, "drop" => Objectives::DROPPED, "undo" => Objectives::ACTIVE }.freeze
|
|
34
29
|
|
|
35
30
|
# @param engine [Engine] #session, #session_state_dir
|
|
36
31
|
def initialize(engine:)
|
|
@@ -84,7 +79,7 @@ module Samagotchi
|
|
|
84
79
|
end
|
|
85
80
|
|
|
86
81
|
def run_line(args, was: nil)
|
|
87
|
-
session = @engine.session or return [
|
|
82
|
+
session = @engine.session or return [Session::NONE_YET, :error]
|
|
88
83
|
# The running turn's moves aren't in session.messages yet. A snapshot from
|
|
89
84
|
# this thread, read only: an entry is there whole or not yet, and a call
|
|
90
85
|
# with no result yet pairs with nothing.
|
|
@@ -93,7 +88,7 @@ module Samagotchi
|
|
|
93
88
|
verb, rest = args.to_s.strip.split(/\s+/, 2)
|
|
94
89
|
return [listing(list, session), :ok] if verb.nil?
|
|
95
90
|
|
|
96
|
-
edited =
|
|
91
|
+
edited = Objectives::Edit.apply(list, verb.downcase, rest.to_s, was: was)
|
|
97
92
|
return [USAGE, :error] if edited.nil?
|
|
98
93
|
return [edited, :error] if edited.is_a?(String)
|
|
99
94
|
return ["#{listing(list, session)}\n(no change)", :ok] if edited == list
|
|
@@ -101,70 +96,10 @@ module Samagotchi
|
|
|
101
96
|
deliver(edited, session)
|
|
102
97
|
end
|
|
103
98
|
|
|
104
|
-
# @return [Objectives::List, String, nil] the new list, what's wrong with
|
|
105
|
-
# the line, or nil for a line that isn't one
|
|
106
|
-
def edit(list, verb, rest, was: nil)
|
|
107
|
-
case verb
|
|
108
|
-
when "set", "add"
|
|
109
|
-
entries = entries_of(rest)
|
|
110
|
-
return nil if entries.nil? || entries.empty?
|
|
111
|
-
|
|
112
|
-
verb == "set" ? list.set(entries) : entries.reduce(list) { |so_far, e| so_far.add(e[:text], done_when: e[:done_when]) }
|
|
113
|
-
when "edit" then reword(list, rest, was: was)
|
|
114
|
-
when *MARKS.keys then mark(list, MARKS.fetch(verb), rest)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def mark(list, status, rest)
|
|
119
|
-
ids = rest.upcase.split(/[\s,]+/).reject(&:empty?)
|
|
120
|
-
return nil if ids.empty?
|
|
121
|
-
|
|
122
|
-
missing = ids.reject { |id| list.find(id) }
|
|
123
|
-
return "no objective #{missing.join(", ")}" unless missing.empty?
|
|
124
|
-
|
|
125
|
-
if status == Objectives::DONE
|
|
126
|
-
dropped = ids.select { |id| list.find(id).status == Objectives::DROPPED }
|
|
127
|
-
return "#{dropped.join(", ")} is dropped; #{NAME} undo #{dropped.join(" ")} brings it back" unless dropped.empty?
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
ids.reduce(list) { |so_far, id| so_far.mark(id, status) }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# The objective's new text: the whole rest of the line, a | too.
|
|
134
|
-
def reword(list, rest, was: nil)
|
|
135
|
-
id, text = rest.strip.split(/\s+/, 2)
|
|
136
|
-
entry = text && entry_of(text.gsub(/[[:space:]]+/, " ").strip)
|
|
137
|
-
return nil unless entry
|
|
138
|
-
|
|
139
|
-
id = id.upcase
|
|
140
|
-
item = list.find(id) or return "no objective #{id}"
|
|
141
|
-
return "#{id} has changed since this was proposed: #{item.text}" if was && Objectives.fingerprint(item) != was
|
|
142
|
-
|
|
143
|
-
list.reword(id, entry[:text], done_when: entry[:done_when])
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# One {text:, done_when:} per line or |-part, any whitespace collapsed
|
|
147
|
-
# (Unicode line separators too: the frame is one line per item).
|
|
148
|
-
# @return [Array<Hash>, nil] nil when a part has no text of its own
|
|
149
|
-
def entries_of(rest)
|
|
150
|
-
parts = rest.split(SEPARATOR).map { |part| part.gsub(/[[:space:]]+/, " ").strip }.reject(&:empty?)
|
|
151
|
-
entries = parts.map { |part| entry_of(part) }
|
|
152
|
-
entries.all? ? entries : nil
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def entry_of(part)
|
|
156
|
-
found = Objectives::DONE_WHEN.match(part)
|
|
157
|
-
text = (found ? found[1] : part).sub(STATUS_MARK, "").strip
|
|
158
|
-
return nil if text.empty?
|
|
159
|
-
|
|
160
|
-
done_when = found && found[2]
|
|
161
|
-
{ text: text, done_when: done_when.to_s.empty? ? nil : done_when }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
99
|
def deliver(list, session)
|
|
165
100
|
result = NoteDelivery.deliver(session.id, text: list.text, source: Objectives::SOURCE,
|
|
166
101
|
state_dir: @engine.session_state_dir, objectives: list.to_payload)
|
|
167
|
-
return ["refused: #{
|
|
102
|
+
return ["refused: #{result.refusal}", :error] unless result.delivered?
|
|
168
103
|
|
|
169
104
|
@engine.announce_objectives(session)
|
|
170
105
|
["#{lines_of(list)}\n#{BETWEEN_TURNS}", :ok]
|