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
data/docs/testing.md
CHANGED
|
@@ -30,6 +30,11 @@ stage's hand-off dwell in stage.spec.js); a pause that is the behaviour outside
|
|
|
30
30
|
route) carries `// eslint-disable-line no-restricted-syntax -- why`. And in the seven
|
|
31
31
|
modules that hold the owner page's session state, one more: no write into `current`, `store.current` or `state()`
|
|
32
32
|
in place (`samagotchi/session-writes`; the store is the write path, see internals/web-frontend.md, The session store).
|
|
33
|
+
In `spec/web/public/`, one rule only (`samagotchi/dom-null-asserts`): no `assert.equal(domNode, null)`. When it fails,
|
|
34
|
+
node's assert formats the happy-dom node, whose graph (its document, its window) takes minutes and gigabytes, so
|
|
35
|
+
`npm test` hangs instead of failing (`--test-timeout` doesn't end it). Write `assert.ok(x === null)` (or `=== undefined`),
|
|
36
|
+
which names the expression and fails at once. The rule sees a DOM lookup by its shape (`querySelector`, `closest`,
|
|
37
|
+
`parentElement`…); a helper that returns a node (`h.input()`) isn't seen, so the same form goes there by hand.
|
|
33
38
|
|
|
34
39
|
CI's e2e job keeps the tests, chi and the fake model on the runner but runs Chromium in Playwright's image
|
|
35
40
|
(`mcr.microsoft.com/playwright:v<version>-noble`, `playwright run-server` on the host network); the tests connect
|
|
@@ -39,7 +44,9 @@ moves the image too (`spec/ci_playwright_image_spec.rb` keeps a pinned tag out o
|
|
|
39
44
|
that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3333/ npm run e2e`.
|
|
40
45
|
|
|
41
46
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
42
|
-
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
47
|
+
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock). `TMPDIR` is a folder of the
|
|
48
|
+
run's own (`SPEC_TMPDIR`, removed at the end), so a spec's bare `Dir.mktmpdir` or `Dir.tmpdir`, and what chi writes or
|
|
49
|
+
sweeps beside it, never touches your real `$TMPDIR`. A spec can't assume that path's length: it is your `$TMPDIR`'s plus one folder.
|
|
43
50
|
It also clears what tells git which repo to act on (`GIT_DIR`, `GIT_WORK_TREE`, the rest of
|
|
44
51
|
`git rev-parse --local-env-vars`: `GitEnv`), first thing in `spec_helper.rb` and as the bundle test kit's
|
|
45
52
|
adapters load, so a run from `git rebase --exec`, `bisect run` or a hook can't reach the real repo through a spec's git.
|
|
@@ -53,6 +60,21 @@ paths (replace them with placeholders), no PATH tools (the suite pins `rg` as fo
|
|
|
53
60
|
that sets PATH to test the lookup is tagged `:real_rg`), no Ruby-version-dependent `inspect` (a Hash's or an
|
|
54
61
|
exception's text changes between Ruby versions: write the string yourself).
|
|
55
62
|
|
|
63
|
+
The whole-turn goldens (`spec/engine_turn_golden_spec.rb`, `spec/fixtures/turn_goldens/`) run a real
|
|
64
|
+
`Engine#run_turn` (its own KernelLoop, ChatLoop, ToolRunner, Gate and GuardrailWiring) over `FakeProviderServer` on
|
|
65
|
+
the native (llama.cpp `/completion`, Qwen tool calls), `api: openai` and `api: anthropic` wires: a turn of two tool
|
|
66
|
+
iterations (a muted `memory_read` and a `read`, then a `write` a config rule asks about, allowed through the question
|
|
67
|
+
flow), a `before_turn` hook switching the model (the request names the new model with the old one's sampling), and a
|
|
68
|
+
review participant's turn whose `write` the review boundary refuses. Each golden pins every request (the chi-set
|
|
69
|
+
headers, the body in its key order, the native prompt as text), the turn's events (keys sorted; the context-status
|
|
70
|
+
estimates and clock values masked), each `before_tool_call` payload, the approval asked and the session's messages.
|
|
71
|
+
System 1, the toolkit suggestion, the context judge, the recap and the warm-up are off by config, so the order is the
|
|
72
|
+
turn's own. The arch P3 refactors (TurnContext, turn stages, ToolSurface) keep them byte-identical: a phase that
|
|
73
|
+
changes one stops and asks. An intended change elsewhere: `UPDATE_PROMPTS=1 bundle exec rspec
|
|
74
|
+
spec/engine_turn_golden_spec.rb`, then read `git diff spec/fixtures/turn_goldens/`; a failure writes what the run got
|
|
75
|
+
to `tmp/turn_goldens/<golden>.actual` and names the `diff` to run. Changes to the turn path (engine, kernel loop, chat loop,
|
|
76
|
+
tool runner, gate, guardrail wiring, system prompt, tool declarations, registry) pick it in `script/affected-specs`.
|
|
77
|
+
|
|
56
78
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
|
|
57
79
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
58
80
|
`question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
@@ -74,11 +96,11 @@ back (`t.mock` restores itself). A test that sets off a module's timer (a flash,
|
|
|
74
96
|
node running until it fires (a 4 s one made the run 6 s for 2 s of work). One file:
|
|
75
97
|
`node --test spec/web/public/NAME.test.js`.
|
|
76
98
|
|
|
77
|
-
Where a composer line goes is decided twice, in Ruby (`ComposerRoute`,
|
|
99
|
+
Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, the attached terminal) and in the web
|
|
78
100
|
(`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
|
|
79
101
|
(`spec/composer_route_spec.rb`) and `npm test` (`spec/web/public/composer.test.js`) run every row. A change to
|
|
80
|
-
either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web`
|
|
81
|
-
|
|
102
|
+
either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web`
|
|
103
|
+
override and a `note`. The Ruby side runs each row with every core command (a new one fails its
|
|
82
104
|
`CORE_ROUTES` check until listed there); a row's names that core lacks stand for plugin commands.
|
|
83
105
|
|
|
84
106
|
## Levels
|
|
@@ -174,6 +196,9 @@ whose value is dropped, an example's last line included, `expect(wait_until {
|
|
|
174
196
|
`Timeout.timeout`, or a statement `thread.join(N)`, whose N is unscaled and whose nil is dropped: use `join!`) live in `rubocop/` and run in `rake lint`, so CI fails on them. RSpec counts `wait_for`,
|
|
175
197
|
`eventually`, `join!` and `pop!` as an example's expectation (`RSpec/NoExpectationExample`), so a wait can be an
|
|
176
198
|
example's last line. What the cops can't see, `script/affected-specs` hints at (`timing:`, below).
|
|
199
|
+
One lib cop sits beside them: `Samagotchi/SilentRescue` (a `rescue` of StandardError whose body is only a literal,
|
|
200
|
+
`nil`, `[]`…: use `Degrade`, or tag a right silence `# degrade-ok: why`; docs/internals/engine.md, Degrading). The
|
|
201
|
+
files that had such rescues when it came are excluded in `.rubocop.yml`: that list only shrinks.
|
|
177
202
|
|
|
178
203
|
### Flakes
|
|
179
204
|
|
|
@@ -308,8 +333,8 @@ A builder runs these while working, not the full suite: the specs the change tou
|
|
|
308
333
|
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…;
|
|
309
334
|
`lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
|
|
310
335
|
docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
|
|
311
|
-
file's layer (any `
|
|
312
|
-
words the
|
|
336
|
+
file's layer (any `foo/*.rb` → `spec/foo_golden_spec.rb` when there is one; `turn_flow.rb` and `turn_note.rb`, whose
|
|
337
|
+
words the attached TUI prints, pick its `event_renderer` and `attached_loop` specs by a rule). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
|
|
313
338
|
effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
|
|
314
339
|
tools. Then come the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
315
340
|
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model_profile"
|
|
4
|
+
require_relative "output_formatter"
|
|
5
|
+
require_relative "tool_call_parser"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
# What the user saw of a model entry's text: the one rule for the continue
|
|
9
|
+
# note's last answer (SessionChain.last_answer), a check's record of the
|
|
10
|
+
# agent's own words (ObjectivesCheck) and a bundle's ctx.last_answer. A chat
|
|
11
|
+
# entry's content is that already (its reasoning is the separate :thinking,
|
|
12
|
+
# its calls :tool_calls); a native entry's is the raw response, with its
|
|
13
|
+
# thoughts and call markup inline: both hosts' thoughts come out, a thought
|
|
14
|
+
# cut off before it closed too, then the calls and the wire tokens.
|
|
15
|
+
module AnswerText
|
|
16
|
+
# The template opened the block, so the text starts inside it.
|
|
17
|
+
TEMPLATE_OPENED_THINK = %r{\A.*?</think>}m
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# @param text [String, nil]
|
|
22
|
+
# @return [String] "" when nothing was visible
|
|
23
|
+
def visible(text)
|
|
24
|
+
text = text.to_s.scrub
|
|
25
|
+
template_opened = !text.include?("<think>")
|
|
26
|
+
gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
|
|
27
|
+
text = gemma.strip_thought(text).gsub(OutputFormatter::QWEN_THINK_RE, "")
|
|
28
|
+
text = text.sub(TEMPLATE_OPENED_THINK, "") if template_opened
|
|
29
|
+
text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
|
|
30
|
+
OutputFormatter.strip_markup(text) # drops a thought cut off before it closed
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -9,16 +9,22 @@ module Samagotchi
|
|
|
9
9
|
# the recap share (Steer.turn_opening?); the store's turn log only orders
|
|
10
10
|
# the ids. So a turn with no opening (a reminder, a Continue, a failed or
|
|
11
11
|
# rolled-back one, a prompt stopped before the model) counts nothing, and
|
|
12
|
-
# an opening with no id (/continue no's reason
|
|
13
|
-
#
|
|
14
|
-
# an entry says how many it came after (+past_idless+, #idless_after)
|
|
15
|
-
#
|
|
12
|
+
# an opening with no turn id (/continue no's reason) counts as it does on
|
|
13
|
+
# the page. Nothing orders such an opening against an entry, so
|
|
14
|
+
# an entry says how many it came after (+past_idless+, #idless_after), and
|
|
15
|
+
# which one that is when it has an id (+after_opening+, #after_opening).
|
|
16
|
+
#
|
|
17
|
+
# The same place as a stamp (#opening_of): the id of the opening the entry
|
|
18
|
+
# comes after (a prompt's turn_id, a line's opening_id), which the page
|
|
19
|
+
# finds by its bubble, not by counting. Pure: no Engine, no file.
|
|
16
20
|
class CardPlaces
|
|
17
21
|
# @param turn_log [Array<String>] the turn ids, oldest first
|
|
18
22
|
# @param messages [Array<Hash>] the messages the page draws
|
|
19
23
|
def initialize(turn_log, messages)
|
|
20
24
|
positions = turn_log.each_with_index.to_h
|
|
21
|
-
|
|
25
|
+
openings = Array(messages).select { |message| Steer.turn_opening?(message) }
|
|
26
|
+
@ids = openings.map { |message| message[:turn_id] || message["turn_id"] }
|
|
27
|
+
@stamps = openings.map { |message| stamp_of(message) }
|
|
22
28
|
@openings = @ids.size
|
|
23
29
|
@opened = @ids.compact.to_set
|
|
24
30
|
@places = @ids.map { |id| positions[id] }
|
|
@@ -45,13 +51,44 @@ module Samagotchi
|
|
|
45
51
|
entry[:base_since].to_i + since
|
|
46
52
|
end
|
|
47
53
|
|
|
54
|
+
# The opening +entry+ comes after, as #turns_since counts: its id (a
|
|
55
|
+
# prompt's turn_id, a line's opening_id), the page's stamp. nil when it
|
|
56
|
+
# comes before every opening, when that opening has no id (a line an
|
|
57
|
+
# older chi wrote), or for an entry from an older file (+base_since+):
|
|
58
|
+
# the page counts those (turns_since).
|
|
59
|
+
# @param entry [Hash] a store's entry: :turn_id, :past_idless, :after_opening, :base_since
|
|
60
|
+
# @return [String, nil]
|
|
61
|
+
def opening_of(entry)
|
|
62
|
+
return nil if entry[:base_since]
|
|
63
|
+
|
|
64
|
+
at_or_before = counted_from(entry[:turn_id])
|
|
65
|
+
return nil unless at_or_before
|
|
66
|
+
|
|
67
|
+
run = at_or_before..(at_or_before + idless_run(at_or_before))
|
|
68
|
+
# The line it saved it came after, while that line is in the run (as
|
|
69
|
+
# the count past it, the two save together); else the count's.
|
|
70
|
+
after = entry[:after_opening] && @stamps.index(entry[:after_opening])
|
|
71
|
+
@stamps[after && run.cover?(after) ? after : [at_or_before + entry[:past_idless].to_i, run.end].min]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The id of the last opening with no turn id right after the one
|
|
75
|
+
# +turn_id+ counts from (#idless_after's run), nil when there is none or
|
|
76
|
+
# it has no id either.
|
|
77
|
+
# @return [String, nil]
|
|
78
|
+
def after_opening(turn_id)
|
|
79
|
+
at_or_before = counted_from(turn_id)
|
|
80
|
+
return nil unless at_or_before
|
|
81
|
+
|
|
82
|
+
run = idless_run(at_or_before)
|
|
83
|
+
run.positive? ? @stamps[at_or_before + run] : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
48
86
|
# The openings with no id right after the one +turn_id+ counts from: what
|
|
49
87
|
# an entry shown now has come after. A rollback can only shorten the run, so
|
|
50
88
|
# a count read back against it never eats a later turn.
|
|
51
89
|
# @return [Integer]
|
|
52
90
|
def idless_after(turn_id)
|
|
53
|
-
|
|
54
|
-
at_or_before = position >= 0 ? @last_at_or_before[position] : nil
|
|
91
|
+
at_or_before = counted_from(turn_id)
|
|
55
92
|
at_or_before ? idless_run(at_or_before) : 0
|
|
56
93
|
end
|
|
57
94
|
|
|
@@ -70,6 +107,18 @@ module Samagotchi
|
|
|
70
107
|
|
|
71
108
|
private
|
|
72
109
|
|
|
110
|
+
# The last opening whose turn is at or before +turn_id+'s in the log
|
|
111
|
+
# (an index into the openings), nil for none.
|
|
112
|
+
def counted_from(turn_id)
|
|
113
|
+
position = @positions.fetch(turn_id, -1)
|
|
114
|
+
position >= 0 ? @last_at_or_before[position] : nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def stamp_of(message)
|
|
118
|
+
id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
|
|
119
|
+
id.is_a?(String) && !id.empty? ? id : nil
|
|
120
|
+
end
|
|
121
|
+
|
|
73
122
|
def idless_run(opening)
|
|
74
123
|
@ids.drop(opening + 1).take_while(&:nil?).size
|
|
75
124
|
end
|
|
@@ -26,7 +26,9 @@ module Samagotchi
|
|
|
26
26
|
# holds the id of the turn it belongs to or follows (+turn_id+: the running
|
|
27
27
|
# turn's, else the last one's, nil before any) and lists with +turns_since+,
|
|
28
28
|
# the turns drawn after it, counted from the messages the page draws
|
|
29
|
-
# (CardPlaces, with the turn log the store keeps),
|
|
29
|
+
# (CardPlaces, with the turn log the store keeps), +opening_id+, the same
|
|
30
|
+
# place as a stamp (the id of the opening it comes after, which the page
|
|
31
|
+
# finds by its bubble; none where only the count can say), and +current+ for a card
|
|
30
32
|
# of the turn running now. A card that isn't the turn's but comes while one
|
|
31
33
|
# runs (an anytime command's, such as /btw) goes after that turn: its prompt
|
|
32
34
|
# is already in the history. Listed against messages, an in-turn entry of a
|
|
@@ -70,7 +72,7 @@ module Samagotchi
|
|
|
70
72
|
SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
71
73
|
question_cancelled question_relay] + Events::TURN_END).freeze
|
|
72
74
|
# What only the store reads: never listed.
|
|
73
|
-
INTERNAL_FIELDS = %i[turn_id continued base_since past_idless during after_failed command].freeze
|
|
75
|
+
INTERNAL_FIELDS = %i[turn_id continued base_since past_idless after_opening during after_failed command].freeze
|
|
74
76
|
|
|
75
77
|
# What a session's saved file lists, as a store started from it lists
|
|
76
78
|
# (no turn running): the web's cards for a session no worker runs.
|
|
@@ -118,7 +120,8 @@ module Samagotchi
|
|
|
118
120
|
# them (a body re-read, a trace): no turns_since:, nothing to tell a turn
|
|
119
121
|
# that left nothing by.
|
|
120
122
|
# @return [Array<Hash>] oldest first: the card's (or notice's) fields
|
|
121
|
-
# plus current:, turns_since:
|
|
123
|
+
# plus current:, turns_since: and opening_id: (CardPlaces#opening_of,
|
|
124
|
+
# when it has one) with +messages+, earlier: true for one an
|
|
122
125
|
# earlier worker saved, during: true for a card that came while the
|
|
123
126
|
# running turn runs but isn't that turn's (it goes after the running
|
|
124
127
|
# turn's prompt), and after_failed: true for one shown after the last
|
|
@@ -141,6 +144,8 @@ module Samagotchi
|
|
|
141
144
|
|
|
142
145
|
out = shown.except(*INTERNAL_FIELDS)
|
|
143
146
|
out[:turns_since] = places.turns_since(entry) if places
|
|
147
|
+
stamp = places&.opening_of(entry)
|
|
148
|
+
out[:opening_id] = stamp if stamp
|
|
144
149
|
out[:current] = current
|
|
145
150
|
out[:during] = true if entry[:during]
|
|
146
151
|
out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
|
|
@@ -255,7 +260,7 @@ module Samagotchi
|
|
|
255
260
|
# A hand-edited or foreign file's entry goes, rather than break every later
|
|
256
261
|
# listing of the session.
|
|
257
262
|
def sound_place?(entry)
|
|
258
|
-
return false unless entry[:turn_id].nil? ||
|
|
263
|
+
return false unless [entry[:turn_id], entry[:after_opening]].all? { |id| id.nil? || id.is_a?(String) }
|
|
259
264
|
|
|
260
265
|
entry.values_at(:base_since, :past_idless).compact.all? { |count| count.is_a?(Integer) && !count.negative? }
|
|
261
266
|
end
|
|
@@ -315,8 +320,8 @@ module Samagotchi
|
|
|
315
320
|
shown = @entries.select { |entry| entry[:turn_id] == @running_turn && !entry[:in_turn] }
|
|
316
321
|
return if shown.empty? || !@messages
|
|
317
322
|
|
|
318
|
-
|
|
319
|
-
shown.each { |entry| entry
|
|
323
|
+
places = CardPlaces.new(@turn_log, @messages.call)
|
|
324
|
+
shown.each { |entry| note_idless(entry, places) }
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
# The entry's anchor: the running turn, else the last one.
|
|
@@ -331,7 +336,7 @@ module Samagotchi
|
|
|
331
336
|
# button's command replaced, which nobody typed).
|
|
332
337
|
old = @entries[index]
|
|
333
338
|
@entries[index] = card.except(:command).merge(updated: true, **old.slice(:in_turn, :turn_id, :during, :continued, :base_since,
|
|
334
|
-
:past_idless, :after_failed, :command))
|
|
339
|
+
:past_idless, :after_opening, :after_failed, :command))
|
|
335
340
|
else
|
|
336
341
|
entry = card.merge(in_turn: event[:in_turn] ? true : false, turn_id: anchor)
|
|
337
342
|
entry[:during] = true if @running_turn && !event[:in_turn]
|
|
@@ -393,8 +398,18 @@ module Samagotchi
|
|
|
393
398
|
def note_past_idless(entry)
|
|
394
399
|
return unless @messages
|
|
395
400
|
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
note_idless(entry, CardPlaces.new(@turn_log, @messages.call))
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# How many openings with no turn id +entry+ came after, and the last
|
|
405
|
+
# one's own id when it has one (+after_opening+: its stamp, #list).
|
|
406
|
+
def note_idless(entry, places)
|
|
407
|
+
run = places.idless_after(entry[:turn_id])
|
|
408
|
+
return unless run.positive?
|
|
409
|
+
|
|
410
|
+
entry[:past_idless] = run
|
|
411
|
+
after = places.after_opening(entry[:turn_id])
|
|
412
|
+
entry[:after_opening] = after if after
|
|
398
413
|
end
|
|
399
414
|
|
|
400
415
|
# Which cap +entry+ counts against: :notice, :llm_context or :card.
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "openssl"
|
|
|
9
9
|
require "time"
|
|
10
10
|
require_relative "atomic_file"
|
|
11
11
|
require_relative "bridge_secret"
|
|
12
|
+
require_relative "degrade"
|
|
12
13
|
require_relative "delivery"
|
|
13
14
|
require_relative "session_inbox"
|
|
14
15
|
require_relative "turn_anchors"
|
|
@@ -156,6 +157,9 @@ module Samagotchi
|
|
|
156
157
|
# Open streams by the client_id a stream named (`?client_id=`); web
|
|
157
158
|
# tabs, through the `chi web` proxy, name none.
|
|
158
159
|
@streams_by_client = Hash.new(0)
|
|
160
|
+
# Streams served since the start, open or closed (a scratch worker
|
|
161
|
+
# leaves once its terminal's has gone: Worker#scratch_unattended?).
|
|
162
|
+
@streams_served = 0
|
|
159
163
|
@last_client_activity_at = monotonic_now
|
|
160
164
|
end
|
|
161
165
|
|
|
@@ -178,6 +182,11 @@ module Samagotchi
|
|
|
178
182
|
@mutex.synchronize { @open_streams }
|
|
179
183
|
end
|
|
180
184
|
|
|
185
|
+
# @return [Integer] SSE streams served since the Bridge started
|
|
186
|
+
def streams_served
|
|
187
|
+
@mutex.synchronize { @streams_served }
|
|
188
|
+
end
|
|
189
|
+
|
|
181
190
|
# Streams held by anyone but +client_id+: a client's own stream (or two,
|
|
182
191
|
# while it reconnects and the old one isn't noticed dead yet) doesn't
|
|
183
192
|
# count. A closed stream stops counting once its writer reads the
|
|
@@ -187,6 +196,15 @@ module Samagotchi
|
|
|
187
196
|
@mutex.synchronize { @open_streams - (client_id ? @streams_by_client[client_id] : 0) }
|
|
188
197
|
end
|
|
189
198
|
|
|
199
|
+
# Streams held by the clients whose id starts with +prefix+
|
|
200
|
+
# (ClientId::TUI_PREFIX: the attached terminals, which a scratch worker
|
|
201
|
+
# counts, Worker#scratch_unattended?). A stream that named no client
|
|
202
|
+
# (a web tab's, through the chi web proxy) is none of them.
|
|
203
|
+
# @return [Integer]
|
|
204
|
+
def open_streams_of(prefix)
|
|
205
|
+
@mutex.synchronize { @streams_by_client.sum { |client_id, count| client_id.start_with?(prefix) ? count : 0 } }
|
|
206
|
+
end
|
|
207
|
+
|
|
190
208
|
# Monotonic time of the last request, stream open or stream close. The
|
|
191
209
|
# worker's idle exit counts from it. A connect that sends no request (the
|
|
192
210
|
# sidecar liveness probe) doesn't count.
|
|
@@ -236,7 +254,7 @@ module Samagotchi
|
|
|
236
254
|
@mutex.synchronize { @stopped = true }
|
|
237
255
|
begin
|
|
238
256
|
@server&.close
|
|
239
|
-
rescue StandardError
|
|
257
|
+
rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
|
|
240
258
|
nil
|
|
241
259
|
end
|
|
242
260
|
wake_acceptor
|
|
@@ -254,7 +272,7 @@ module Samagotchi
|
|
|
254
272
|
@mutex.synchronize { @stopped = true }
|
|
255
273
|
begin
|
|
256
274
|
@server&.close
|
|
257
|
-
rescue StandardError
|
|
275
|
+
rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
|
|
258
276
|
nil
|
|
259
277
|
end
|
|
260
278
|
wake_acceptor
|
|
@@ -268,13 +286,13 @@ module Samagotchi
|
|
|
268
286
|
# the web, which then queues the prompt again from the input file.
|
|
269
287
|
await_answers(REQUEST_GRACE_SECONDS)
|
|
270
288
|
remove_sidecar
|
|
271
|
-
@connection_threads.each { |t| t.kill rescue nil }
|
|
289
|
+
@connection_threads.each { |t| t.kill rescue nil } # degrade-ok: stopping: a thread already gone is fine
|
|
272
290
|
@connection_threads.clear
|
|
273
291
|
# Its relay watchers (RelayWatcher) end with it.
|
|
274
292
|
watchers = @mutex.synchronize { (@relay_watchers || []).tap { @relay_watchers = [] } }
|
|
275
293
|
watchers.each { |thread| thread.kill.join(RELAY_WATCHER_JOIN_SECONDS) }
|
|
276
294
|
@accept_thread&.join(2)
|
|
277
|
-
[@wake_reader, @wake_writer].each { |io| io&.close rescue nil }
|
|
295
|
+
[@wake_reader, @wake_writer].each { |io| io&.close rescue nil } # degrade-ok: stopping: a closed pipe is fine
|
|
278
296
|
nil
|
|
279
297
|
end
|
|
280
298
|
|
|
@@ -399,9 +417,10 @@ module Samagotchi
|
|
|
399
417
|
# O(1) and non-blocking.
|
|
400
418
|
def capture_observer
|
|
401
419
|
@capture_proc ||= proc do |event|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
420
|
+
# Per event: the first failure warns, the rest log at debug.
|
|
421
|
+
Degrade.call(:bridge, "event_capture_failed") do
|
|
422
|
+
@ring.push(seq: event[:event_seq], data: event)
|
|
423
|
+
end
|
|
405
424
|
end
|
|
406
425
|
end
|
|
407
426
|
|
|
@@ -527,7 +546,7 @@ module Samagotchi
|
|
|
527
546
|
Thread.current[:bridge_answering] = false
|
|
528
547
|
begin
|
|
529
548
|
io.close
|
|
530
|
-
rescue StandardError
|
|
549
|
+
rescue StandardError # degrade-ok: the client may be gone already; closing is all that's left
|
|
531
550
|
nil
|
|
532
551
|
end
|
|
533
552
|
end
|
|
@@ -555,6 +574,7 @@ module Samagotchi
|
|
|
555
574
|
)
|
|
556
575
|
@mutex.synchronize do
|
|
557
576
|
@open_streams += 1
|
|
577
|
+
@streams_served += 1
|
|
558
578
|
@streams_by_client[client_id] += 1 if client_id
|
|
559
579
|
end
|
|
560
580
|
begin
|
|
@@ -860,7 +880,7 @@ module Samagotchi
|
|
|
860
880
|
end
|
|
861
881
|
end
|
|
862
882
|
|
|
863
|
-
# Dismiss the pending question (an empty answer
|
|
883
|
+
# Dismiss the pending question (an empty answer at a terminal): the
|
|
864
884
|
# tool returns without an answer and every UI gets :question_cancelled.
|
|
865
885
|
# Only the question the client saw, and only while nobody has answered
|
|
866
886
|
# it: Engine#cancel_question checks both under the question lock. A past
|
|
@@ -1425,44 +1445,44 @@ module Samagotchi
|
|
|
1425
1445
|
# else fall back to the explicit `?from_seq=` query param used by clients
|
|
1426
1446
|
# that remember their own cursor.
|
|
1427
1447
|
def reconnect_cursor(headers, query)
|
|
1428
|
-
|
|
1429
|
-
|
|
1448
|
+
Degrade.call(:bridge, "sse_cursor_unparsable") do
|
|
1449
|
+
cursor = headers["last-event-id"]
|
|
1450
|
+
return cursor if cursor && !cursor.empty?
|
|
1430
1451
|
|
|
1431
|
-
|
|
1452
|
+
return nil unless query
|
|
1432
1453
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
nil
|
|
1454
|
+
URI.decode_www_form(query).to_h["from_seq"]
|
|
1455
|
+
end
|
|
1436
1456
|
end
|
|
1437
1457
|
|
|
1438
1458
|
# `?snapshot=1`: join with a snapshot frame instead of a replay.
|
|
1439
1459
|
def snapshot_requested?(query)
|
|
1440
|
-
|
|
1460
|
+
Degrade.call(:bridge, "snapshot_query_unparsable", fallback: false) do
|
|
1461
|
+
return false unless query
|
|
1441
1462
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
false
|
|
1463
|
+
URI.decode_www_form(query).to_h["snapshot"].to_s == "1"
|
|
1464
|
+
end
|
|
1445
1465
|
end
|
|
1446
1466
|
|
|
1447
1467
|
# `?raw=1`: events as the engine emitted them (a chunk's provider
|
|
1448
1468
|
# payload), not as clients get them (ClientEvent). For debugging on the
|
|
1449
1469
|
# worker's port: chi web never passes it on.
|
|
1450
1470
|
def raw_requested?(query)
|
|
1451
|
-
|
|
1471
|
+
Degrade.call(:bridge, "raw_query_unparsable", fallback: false) do
|
|
1472
|
+
return false unless query
|
|
1452
1473
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
false
|
|
1474
|
+
URI.decode_www_form(query).to_h["raw"].to_s == "1"
|
|
1475
|
+
end
|
|
1456
1476
|
end
|
|
1457
1477
|
|
|
1458
1478
|
# `?client_id=`: whose stream it is (see #open_streams_except).
|
|
1459
1479
|
def stream_client_id(query)
|
|
1460
|
-
|
|
1480
|
+
Degrade.call(:bridge, "client_id_query_unparsable") do
|
|
1481
|
+
return nil unless query
|
|
1461
1482
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
nil
|
|
1483
|
+
id = URI.decode_www_form(query).to_h["client_id"].to_s
|
|
1484
|
+
id.empty? ? nil : id
|
|
1485
|
+
end
|
|
1466
1486
|
end
|
|
1467
1487
|
|
|
1468
1488
|
def close_after_request?(headers)
|
|
@@ -1534,11 +1554,11 @@ module Samagotchi
|
|
|
1534
1554
|
# same session may have written its own since (a file without an epoch
|
|
1535
1555
|
# is only ever an older worker's).
|
|
1536
1556
|
def remove_sidecar
|
|
1537
|
-
|
|
1557
|
+
Degrade.call(:bridge, "sidecar_remove_failed") do
|
|
1558
|
+
return unless @server
|
|
1538
1559
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
nil
|
|
1560
|
+
File.unlink(WorkerSidecar.path(session_dir)) if WorkerSidecar.read(session_dir)&.epoch == @epoch.to_s
|
|
1561
|
+
end
|
|
1542
1562
|
end
|
|
1543
1563
|
|
|
1544
1564
|
def session_dir
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "socket"
|
|
6
6
|
|
|
7
|
+
require_relative "degrade"
|
|
7
8
|
require_relative "worker_sidecar"
|
|
8
9
|
require_relative "bridge_secret"
|
|
9
10
|
require_relative "delivery"
|
|
@@ -308,12 +309,16 @@ module Samagotchi
|
|
|
308
309
|
# older worker's 404 for a route it lacks from a refused connect, a
|
|
309
310
|
# timeout or a 500.
|
|
310
311
|
# @return [Array(Integer, Object)] the status and the parsed JSON body
|
|
311
|
-
# (nil when it isn't JSON); [nil, nil] when no reply came
|
|
312
|
+
# (nil when it isn't JSON); [nil, nil] when no reply came (a refused
|
|
313
|
+
# connect, a timeout, a cut or garbled reply), and when anything else
|
|
314
|
+
# failed, which is logged
|
|
312
315
|
def get(path)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
Degrade.call(:bridge, "get_failed", fallback: [nil, nil], path: path) do
|
|
317
|
+
response = get_response(path)
|
|
318
|
+
[response.status, response.json]
|
|
319
|
+
rescue SystemCallError, IOError
|
|
320
|
+
[nil, nil]
|
|
321
|
+
end
|
|
317
322
|
end
|
|
318
323
|
|
|
319
324
|
# GET /session/:id/<path>, preserving the raw response and raising on I/O.
|
|
@@ -329,17 +334,17 @@ module Samagotchi
|
|
|
329
334
|
body = response.split("\r\n\r\n", 2)[1] || ""
|
|
330
335
|
Response.new(status: status.to_i, body: body)
|
|
331
336
|
ensure
|
|
332
|
-
sock&.close rescue nil
|
|
337
|
+
sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
|
|
333
338
|
end
|
|
334
339
|
|
|
335
340
|
# Monotonic SSE cursor of the live Engine, from GET /session/:id/state.
|
|
336
341
|
# @return [Integer, nil]
|
|
337
342
|
def event_seq
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
Degrade.call(:bridge, "event_seq_unreadable") do
|
|
344
|
+
state = get_json("state")
|
|
345
|
+
seq = state && state.dig("session_state_snapshot", "event_seq")
|
|
346
|
+
seq.nil? ? nil : seq.to_i
|
|
347
|
+
end
|
|
343
348
|
end
|
|
344
349
|
|
|
345
350
|
# Open GET /session/:id/stream and classify the response before the
|
|
@@ -358,7 +363,7 @@ module Samagotchi
|
|
|
358
363
|
if connection
|
|
359
364
|
sock, status, headers = connection
|
|
360
365
|
unless !block_given? || yield(sock)
|
|
361
|
-
sock.close rescue nil
|
|
366
|
+
sock.close rescue nil # degrade-ok: the stream is given up; a socket already closed is fine
|
|
362
367
|
return StreamOpen::Unreachable.new(attempts: count)
|
|
363
368
|
end
|
|
364
369
|
return StreamOpen.classify(sock, status, headers)
|
|
@@ -389,7 +394,7 @@ module Samagotchi
|
|
|
389
394
|
break
|
|
390
395
|
end
|
|
391
396
|
ensure
|
|
392
|
-
sock&.close rescue nil
|
|
397
|
+
sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
|
|
393
398
|
end
|
|
394
399
|
end
|
|
395
400
|
|
|
@@ -435,7 +440,7 @@ module Samagotchi
|
|
|
435
440
|
body = read_body ? reply.split("\r\n\r\n", 2)[1] : nil
|
|
436
441
|
Response.new(status: reply[STATUS_LINE, 1].to_i, body: body)
|
|
437
442
|
ensure
|
|
438
|
-
sock&.close rescue nil
|
|
443
|
+
sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
|
|
439
444
|
end
|
|
440
445
|
|
|
441
446
|
# The reply up to the Bridge's close (or its first line only), within
|
|
@@ -12,7 +12,7 @@ module Samagotchi
|
|
|
12
12
|
# A session a broadcast may concern.
|
|
13
13
|
# @!attribute project [String, nil] its git project's root
|
|
14
14
|
# @!attribute live [Boolean] a worker runs it now
|
|
15
|
-
# @!attribute owner [String, nil] "worker", "tui" (a chi
|
|
15
|
+
# @!attribute owner [String, nil] "worker", "tui" (a one-turn chi run, which
|
|
16
16
|
# takes no notes) or nil
|
|
17
17
|
Recipient = Data.define(:id, :short_id, :project, :cwd, :desc, :live, :owner) do
|
|
18
18
|
def repl? = owner == "tui"
|
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
|
|
21
21
|
# Who a broadcast goes to: the user's own sessions, not delegate
|
|
22
22
|
# children (their parent relays what concerns them) and not scratch
|
|
23
|
-
# ones, that a worker or a chi
|
|
23
|
+
# ones, that a worker or a one-turn chi run runs now or that ended a turn in
|
|
24
24
|
# the last +active_hours+. Forks count: they are the user's sessions.
|
|
25
25
|
# Archived sessions don't; test runs only when chi runs as one.
|
|
26
26
|
module Recipients
|
|
@@ -23,7 +23,7 @@ module Samagotchi
|
|
|
23
23
|
# @!attribute by [String] who decided: "tags" (a shared tag),
|
|
24
24
|
# "model", "fallback" (delivered unchecked: deadline, error, an
|
|
25
25
|
# answer that is no yes or no), "scope" (the note's first line
|
|
26
|
-
# names another project), "all" (--all) or "repl" (a chi
|
|
26
|
+
# names another project), "all" (--all) or "repl" (a one-turn chi run owns
|
|
27
27
|
# it: no notes)
|
|
28
28
|
# @!attribute match [Match, nil] the shared tag (by "tags")
|
|
29
29
|
Verdict = Data.define(:relevant, :p, :reason, :by, :match) do
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
note from "broadcast": background the model sees on its next turn,
|
|
47
47
|
not a prompt. It starts no turn.
|
|
48
48
|
Recipients: your own sessions (not delegate children, not scratch
|
|
49
|
-
ones) that a worker or a chi
|
|
49
|
+
ones) that a worker or a one-turn chi run runs now, or that ended a turn in
|
|
50
50
|
the last broadcast.active_hours (8). One gets the note when it shares
|
|
51
51
|
a tag with it: a ticket id (PAY-123) in its branch or your prompts
|
|
52
52
|
there, a pull request (PR #42, its URL) attached to it, a link in
|
|
@@ -190,13 +190,13 @@ module Samagotchi
|
|
|
190
190
|
corrections: []), decisions, cards)
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
-
# Every recipient's verdict: a chi
|
|
193
|
+
# Every recipient's verdict: a one-turn chi run takes no notes, --all takes the
|
|
194
194
|
# rest, else Broadcast::Triage (tags, the scope line, the model).
|
|
195
195
|
# @return [Array<Decision>] in +recipients+' order
|
|
196
196
|
def decide(text, recipients, cards, note_tags, all:)
|
|
197
197
|
fixed = recipients.to_h do |r|
|
|
198
198
|
verdict = if r.repl?
|
|
199
|
-
Broadcast::Triage::Verdict.new(relevant: false, p: nil, reason:
|
|
199
|
+
Broadcast::Triage::Verdict.new(relevant: false, p: nil, reason: OwnerLock::TUI_BUSY, by: "repl")
|
|
200
200
|
elsif all
|
|
201
201
|
Broadcast::Triage::Verdict.new(relevant: true, p: nil, reason: "--all", by: "all")
|
|
202
202
|
end
|
|
@@ -254,7 +254,7 @@ module Samagotchi
|
|
|
254
254
|
|
|
255
255
|
result = NoteDelivery.deliver(d.recipient.id, text: body(broadcast.text, d.verdict), source: SOURCE,
|
|
256
256
|
state_dir: @state_dir)
|
|
257
|
-
next [d, "skipped",
|
|
257
|
+
next [d, "skipped", OwnerLock::TUI_BUSY] unless result.delivered?
|
|
258
258
|
|
|
259
259
|
[d, "delivered", "#{d.verdict.reason}#{"; waits for its next start" if result.status == :waits}"]
|
|
260
260
|
rescue SessionInbox::NoteRejected, SystemCallError => e
|
|
@@ -369,7 +369,7 @@ module Samagotchi
|
|
|
369
369
|
|
|
370
370
|
result = NoteDelivery.deliver(id, text: passed_on_body(record), source: SOURCE, state_dir: @state_dir)
|
|
371
371
|
unless result.delivered?
|
|
372
|
-
@stdout.puts(line_for(id, "skipped",
|
|
372
|
+
@stdout.puts(line_for(id, "skipped", OwnerLock::TUI_BUSY))
|
|
373
373
|
return Broadcast::TriageLog::Delivery.new(session: id, result: "refused")
|
|
374
374
|
end
|
|
375
375
|
|
|
@@ -42,7 +42,7 @@ module Samagotchi
|
|
|
42
42
|
f.switch "--json"
|
|
43
43
|
f.switch "--keep"
|
|
44
44
|
f.value "--timeout"
|
|
45
|
-
f.refuse "--attach", "
|
|
45
|
+
f.refuse "--attach", "chi bundle try can't use --attach"
|
|
46
46
|
end
|
|
47
47
|
DEFAULT_TIMEOUT = 300
|
|
48
48
|
# The sandbox's cost-cap bundle setting (--set cost-cap.cap=N).
|