samagotchi 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -1
- data/bin/chi +60 -191
- data/docs/architecture.md +67 -1
- data/docs/cli.md +190 -9
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +4 -2
- data/docs/internals/context-telemetry.md +6 -2
- data/docs/internals/engine.md +5 -2
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/participants.md +137 -0
- data/docs/internals/remote-sessions.md +429 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +33 -3
- data/docs/sessions.md +16 -7
- data/docs/sub-agent.md +16 -0
- data/docs/testing.md +68 -4
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +31 -7
- data/lib/samagotchi/bridge_client/event_stream.rb +86 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +26 -39
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +40 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/context_status.rb +15 -2
- data/lib/samagotchi/engine.rb +28 -14
- data/lib/samagotchi/guardrail_wiring.rb +7 -1
- data/lib/samagotchi/idle_recap.rb +8 -2
- data/lib/samagotchi/idle_scheduler.rb +34 -9
- data/lib/samagotchi/kernel_loop.rb +21 -2
- data/lib/samagotchi/launch_mode.rb +26 -0
- data/lib/samagotchi/llm/chat_loop.rb +8 -1
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/owner_lock.rb +65 -5
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/participants.rb +122 -0
- data/lib/samagotchi/remote_attach_command.rb +119 -0
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +245 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +38 -9
- data/lib/samagotchi/session_access/contract.rb +167 -0
- data/lib/samagotchi/session_access/dispatch.rb +152 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +400 -0
- data/lib/samagotchi/session_access/remote.rb +734 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +236 -0
- data/lib/samagotchi/session_access.rb +223 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_inbox.rb +18 -9
- data/lib/samagotchi/session_manager.rb +27 -6
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/share_command.rb +174 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +66 -17
- data/lib/samagotchi/terminal_ui/attach_link.rb +114 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +155 -87
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +135 -0
- data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
- data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +28 -3
- data/lib/samagotchi/terminal_ui.rb +6 -3
- data/lib/samagotchi/testing/waiting.rb +92 -9
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/tools/task_runtime.rb +17 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +530 -126
- data/lib/samagotchi/web/grants.rb +302 -0
- data/lib/samagotchi/web/participant_access.rb +286 -0
- data/lib/samagotchi/web/principal.rb +29 -0
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/server.rb +3 -1
- data/lib/samagotchi/web/session_access_routes.rb +459 -0
- data/lib/samagotchi/worker.rb +26 -5
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- metadata +28 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4a794e9e201d233ca72705b62746997d6ef702804e18ad8a5bb1f85851ed454
|
|
4
|
+
data.tar.gz: 341e3b0cfcbc4367faed3782d600b2d934395eef7623499619f018b1984a06db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20bdcd9c915786fe5698401079a36e676ace823d4e42625c5b75fd2472d9990b342f1cc9c23e2a609bffee3b57df88c4fb9b6c927cdf0e182c1f7966fc5fde37
|
|
7
|
+
data.tar.gz: 64d05e8f627bb8c628e3510681b7acf1de4c37995175333614094d00333f66b1de28d91a6b2819f005e4d1e5150c8ffd189f6c9e2379044b89268188086231e6
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,96 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.104.0] - 2026-10-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Remote attach: `chi --remote NAME --attach ID` (and `--resume ID`, `-p TEXT`, or `--dir DIR` for a new session
|
|
16
|
+
there, that machine's absolute path) runs the attached TUI on another machine's session, through its chi web:
|
|
17
|
+
the same lines and exit codes, every printed command spelled with `--remote NAME`, `@path` images from this
|
|
18
|
+
machine, a "(reconnecting…)" / "(reconnected)" line for about a minute of reconnects, and the version line naming
|
|
19
|
+
both machines. `--non-interactive`, `--verbose`, `--no-shared`, `--memory`, `--mute` and
|
|
20
|
+
`--append-system-prompt` stay this machine's (exit 2). `--dir` without `--remote` exits 2. `/exit`,
|
|
21
|
+
`/exit --delete`, `/archive`, `/stats` and `/recap` work through a remote as here, the status row counts the
|
|
22
|
+
session's delegates there, and a join shows the steps' durations and a failed turn's line from that machine's
|
|
23
|
+
saved records. See [docs/cli.md](docs/cli.md#remote-sessions).
|
|
24
|
+
- Participants (experimental: identity only, the review itself comes in a later release): `chi share ID --name NAME
|
|
25
|
+
[--agent] [--expires 3d]` invites someone, or an agent, to review a session through chi web on the LAN with a link
|
|
26
|
+
of their own, printed once with a QR code (chi keeps only a digest of its token). `chi share ls [ID]` lists
|
|
27
|
+
participants and `chi share revoke ID [P] | --all` ends links; all three work through `--remote`. A link works for 7
|
|
28
|
+
days (1 hour to 30 days), until it is revoked, or until the session is archived or deleted. For now it opens a page
|
|
29
|
+
with the session's title and status; every other route answers it 403, and another session 404. chi web refuses to
|
|
30
|
+
start when another user can read its grants store. Through `--remote`, a revoked, expired or participant's token
|
|
31
|
+
gets its own message instead of "the token was refused". See [docs/cli.md](docs/cli.md#participants).
|
|
32
|
+
- The bundle test kit (experimental) waits with `wait_for(what) { … }`, which raises
|
|
33
|
+
`Samagotchi::Testing::Waiting::TimedOut` naming what it waited for (and, with `seen:`, what it last saw), plus
|
|
34
|
+
`join!` and `pop!` with the same message. Every helper deadline is multiplied by `SPEC_TIME_SCALE` (default 1).
|
|
35
|
+
See [docs/plugins.md](docs/plugins.md#testing-a-bundle-experimental).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The bundle test kit's `wait_until!` is renamed `wait_for`, with no alias: a bundle spec that calls
|
|
40
|
+
`wait_until!` changes the name (same arguments).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `chi --attach ID` (and `--resume ID`, and `chi --remote NAME --attach ID`) right after `/exit`, while the leaving
|
|
45
|
+
worker still writes its recap, waits for it with `(waiting for the session to finish its recap…)` (up to
|
|
46
|
+
`recap.timeout` plus 5 s) and then attaches, instead of failing "did not start its Bridge in time". A Ctrl-C while
|
|
47
|
+
an attach connects exits 130 without a backtrace.
|
|
48
|
+
- `/queue TEXT` (or `/cut TEXT`) with no turn running says `(nothing is running: sent now)` in the REPL and the
|
|
49
|
+
attached terminal, instead of sending without a word.
|
|
50
|
+
- The ctx % no longer dips at the start of each turn on a chat host (`api: openai` / `anthropic`): the turn's first
|
|
51
|
+
estimate, made before the host counts anything, left out the tool schemas; it now uses the last count's chars per
|
|
52
|
+
token.
|
|
53
|
+
- Attached terminal: a command typed right after the text `/unqueue` put back in the input (`/unqueue` again,
|
|
54
|
+
`/queue TEXT`) runs as the command, with the text kept in the input, instead of going out joined to the text as
|
|
55
|
+
one prompt.
|
|
56
|
+
- `chi --remote NAME sessions stop` waits up to 10 s there for the worker to let go, as `chi sessions stop` does
|
|
57
|
+
here (chi web waited 2 s, so a slow worker read "still shutting down" more often); the page's Stop still waits 2 s.
|
|
58
|
+
- The terminal's status line, cut to the terminal's width, ends at a word with `…` instead of mid-word (in a pipe
|
|
59
|
+
too).
|
|
60
|
+
|
|
61
|
+
Update with `chi update`.
|
|
62
|
+
|
|
63
|
+
## [0.103.0] - 2026-10-23
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- Remote sessions: `chi --remote NAME send|answer|note|context|sessions …` (or `CHI_REMOTE=NAME`) runs the session
|
|
68
|
+
commands against another machine's sessions, through its `chi web --web-host lan`. `chi remote add NAME URL
|
|
69
|
+
--token-file FILE` checks the address and token and saves the remote (`chi remote ls`, `chi remote rm`).
|
|
70
|
+
`send --new` there needs `--dir`, a folder on that machine; what runs on this machine only (attach, `sessions
|
|
71
|
+
stats`, `context mute`, `broadcast`, …) is refused with exit 2. The next-step commands it prints (`chi answer …`,
|
|
72
|
+
`chi send --wait …`) carry `--remote NAME`; where chi points at `chi --attach` here it names that machine's chi web.
|
|
73
|
+
See [docs/cli.md](docs/cli.md#remote-sessions).
|
|
74
|
+
- `/working-set pin KEY…` keeps an item from the model's `forget_outputs`: its reads, those to come too, stay
|
|
75
|
+
whole until `/working-set unpin KEY…`. `/working-set add KEY…` brings an item in: the next turn's line asks the
|
|
76
|
+
model to read it first (one tool call; what it reads is an ordinary read); `/working-set unadd KEY…` takes an
|
|
77
|
+
add back before then. In the web's working set list (still experimental, `web.working_set: true`), rows offer
|
|
78
|
+
`pin` / `unpin`, `+` adds an item, and a stale or forgotten row offers a refresh. See [docs/cli.md](docs/cli.md).
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- The guardrails bundle (0.9.3) asks for `chi --remote NAME context add --cmd …` and `chi --remote NAME broadcast`
|
|
83
|
+
as it does without `--remote`. Update with `chi update`.
|
|
84
|
+
- claude-peer (0.4.2): `pair` through a remote (`CHI_REMOTE`, or `--remote NAME` in `CHI`) attaches only
|
|
85
|
+
`claude-outbox` and says chi there can't send to that Claude. Update with `chi update`.
|
|
86
|
+
- codex-peer (0.3.3): the same for `pair` through a remote: it attaches only `codex-outbox`, says chi there can't
|
|
87
|
+
send to that Codex, and when chi can't reach the remote names Codex's sandbox `network_access` setting as a
|
|
88
|
+
possible cause. Update with `chi update`.
|
|
89
|
+
- chi web's `POST /api/sessions/:id/stop` also answers `released`; `DELETE /api/sessions/:id` takes `force` (the
|
|
90
|
+
page sends it). Both are additive.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- The web's stage shrinks back when you close its steps list by hand, instead of keeping its grown height.
|
|
95
|
+
- `/working-set`: a `memory_read` of several names is dropped, pinned or added as a whole (the reply names the
|
|
96
|
+
others); a quoted KEY keeps its spaces, in the TUI and the web; a file in the session's folder is shown by its
|
|
97
|
+
relative path.
|
|
98
|
+
- The idle recap's summarize thread ends quietly, logged, on a failure.
|
|
99
|
+
- On Linux a worker stops at once (not ~0.5 s later), and its address is gone before its streams end, so an attached UI follows a restarted worker straight away.
|
|
100
|
+
|
|
11
101
|
## [0.102.0] - 2026-10-23
|
|
12
102
|
|
|
13
103
|
### Added
|
|
@@ -3854,7 +3944,9 @@ and long-lived sessions.
|
|
|
3854
3944
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3855
3945
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3856
3946
|
|
|
3857
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3947
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.104.0...HEAD
|
|
3948
|
+
[0.104.0]: https://github.com/dm1try/samagotchi/compare/v0.103.0...v0.104.0
|
|
3949
|
+
[0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
|
|
3858
3950
|
[0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
|
|
3859
3951
|
[0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
|
|
3860
3952
|
[0.100.1]: https://github.com/dm1try/samagotchi/compare/v0.100.0...v0.100.1
|
data/bin/chi
CHANGED
|
@@ -8,7 +8,6 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
|
8
8
|
require "samagotchi/utf8_default"
|
|
9
9
|
Samagotchi::Utf8Default.apply!
|
|
10
10
|
|
|
11
|
-
require "optparse"
|
|
12
11
|
require "samagotchi/config"
|
|
13
12
|
|
|
14
13
|
begin
|
|
@@ -53,6 +52,8 @@ SUBCOMMANDS = {
|
|
|
53
52
|
"send" => ["samagotchi/send_command", "SendCommand"],
|
|
54
53
|
# answer the question a session waits on, then wait (--question, --option)
|
|
55
54
|
"answer" => ["samagotchi/answer_command", "AnswerCommand"],
|
|
55
|
+
# invite participants to review a session, list them, revoke them
|
|
56
|
+
"share" => ["samagotchi/share_command", "ShareCommand"],
|
|
56
57
|
# find a model server and write config.yml (--model, --name)
|
|
57
58
|
"bootstrap" => ["samagotchi/bootstrap_command", "BootstrapCommand"],
|
|
58
59
|
# bring an installed chi up to date
|
|
@@ -64,8 +65,31 @@ SUBCOMMANDS = {
|
|
|
64
65
|
# list and remove memories (ls, rm; --scope, --format)
|
|
65
66
|
"memory" => ["samagotchi/memory_command", "MemoryCommand"],
|
|
66
67
|
# install, upgrade, uninstall, status, diff, list, build (--scope, --force, …)
|
|
67
|
-
"bundle" => ["samagotchi/bundle_command", "BundleCommand"]
|
|
68
|
+
"bundle" => ["samagotchi/bundle_command", "BundleCommand"],
|
|
69
|
+
# other machines' chi web, for chi --remote NAME (add, ls, rm)
|
|
70
|
+
"remote" => ["samagotchi/remote_command", "RemoteCommand"]
|
|
68
71
|
}.freeze
|
|
72
|
+
# chi --remote NAME <command> (or CHI_REMOTE=NAME): the session commands
|
|
73
|
+
# against another machine's chi web; what runs here only is refused (exit 2).
|
|
74
|
+
# chi remote, --version and --help run here as always. Run flags under a
|
|
75
|
+
# remote (--attach, --resume, -p, --dir: a session there in this terminal)
|
|
76
|
+
# are parsed below and go to RemoteAttachCommand before this machine's
|
|
77
|
+
# checks; the local-only ones are refused here.
|
|
78
|
+
require "samagotchi/session_access/dispatch"
|
|
79
|
+
begin
|
|
80
|
+
remote_run = Samagotchi::SessionAccess::Dispatch.from(ARGV)
|
|
81
|
+
rescue Samagotchi::SessionAccess::Dispatch::Usage => e
|
|
82
|
+
warn "Error: #{e.message}"
|
|
83
|
+
exit 2
|
|
84
|
+
end
|
|
85
|
+
ARGV.replace(remote_run.argv)
|
|
86
|
+
remote_attach = remote_run.remote? && !remote_run.here? && remote_run.run_flags?
|
|
87
|
+
if remote_attach && (refusal = remote_run.refusal)
|
|
88
|
+
warn "Error: #{refusal}"
|
|
89
|
+
exit 2
|
|
90
|
+
end
|
|
91
|
+
exit remote_run.run(SUBCOMMANDS) if remote_run.remote? && !remote_run.here? && !remote_attach
|
|
92
|
+
|
|
69
93
|
if (path, klass = SUBCOMMANDS[ARGV[0]])
|
|
70
94
|
require path
|
|
71
95
|
exit Samagotchi.const_get(klass).new(ARGV[1..] || []).run
|
|
@@ -76,185 +100,10 @@ end
|
|
|
76
100
|
scratch = ARGV[0] == "scratch"
|
|
77
101
|
ARGV.shift if scratch
|
|
78
102
|
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Strict kebab enforcement: --recap_base_url is unknown (OptionParser would otherwise accept _ as -)
|
|
84
|
-
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
85
|
-
ARGV.each do |arg|
|
|
86
|
-
next unless arg.start_with?("--") && arg.include?("_")
|
|
87
|
-
|
|
88
|
-
# Allow --no-interrupt etc. already dash; underscore is always rejected for config flags
|
|
89
|
-
# We check raw ARGV before OptionParser normalizes
|
|
90
|
-
base = arg.split("=", 2).first
|
|
91
|
-
# If dashed version is a known flag (registry or hardcoded), reject underscore variant
|
|
92
|
-
dashed = base.tr("_", "-")
|
|
93
|
-
if known.include?(dashed) || known.include?(base)
|
|
94
|
-
warn "Unknown option: #{arg} (did you mean #{dashed}?)"
|
|
95
|
-
exit 1
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
parser = OptionParser.new do |opts|
|
|
100
|
-
opts.banner = <<~BANNER
|
|
101
|
-
Usage: chi [options]
|
|
102
|
-
chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
|
|
103
|
-
chi scratch [options] a one-time session in this terminal: nothing is kept
|
|
104
|
-
chi web [--port PORT] [--open] [--scope=all] the web UI
|
|
105
|
-
chi sessions <list|stop|restart|archive|unarchive|delete|prune|clean|stats>
|
|
106
|
-
chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
|
|
107
|
-
chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
|
|
108
|
-
chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
|
|
109
|
-
chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
|
|
110
|
-
chi answer ID --question QID --option N answer a question a session waits on
|
|
111
|
-
chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
|
|
112
|
-
chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
|
|
113
|
-
chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
|
|
114
|
-
chi models [--format json] [TEXT] the models --model takes, from every host
|
|
115
|
-
chi self version, paths, model and bundles
|
|
116
|
-
chi update [--dry-run] update this chi, its bundles and the desktop helper
|
|
117
|
-
(each subcommand takes --help)
|
|
118
|
-
|
|
119
|
-
BANNER
|
|
120
|
-
opts.on("-p", "--prompt PROMPT", "Run PROMPT as the first turn, then stay in the REPL") { |p| options[:prompt] = p }
|
|
121
|
-
opts.on("--resume SESSION_ID", "Resume a previous session by its ID (or a unique prefix of it)") { |s| options[:resume] = s }
|
|
122
|
-
opts.on("--attach SESSION_ID", "Attach to a session's worker, waking one if it has exited; ID or a unique prefix") { |s| options[:attach] = s }
|
|
123
|
-
opts.on("--[no-]shared", "Run the session in a background worker and attach to it, so `chi web` can share it (the default, config session.shared; with --resume: that session); --no-shared runs a plain in-process REPL") { |v| options[:shared] = v }
|
|
124
|
-
opts.on("--model NAME", "Model for this run, not saved (overrides --resume's; config default.model) [env SAMAGOTCHI_DEFAULT_MODEL]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
|
|
125
|
-
opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
|
|
126
|
-
"Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join("|")}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
|
|
127
|
-
opts.on("--thinking LEVEL", Samagotchi::Thinking::LEVELS.map(&:to_s), "This session's own thinking level: off|low|medium|high|default (saved in the session, " \
|
|
128
|
-
"before the model's; default unsets it; /thinking changes it). With chi web: the default for the sessions " \
|
|
129
|
-
"its workers run, below a session's own [env SAMAGOTCHI_THINKING_LEVEL]") { |l| options[:thinking] = l }
|
|
130
|
-
opts.on("--toolkit NAME", "This session's toolkit, the tools its prompts declare: full|lean|child|a config or bundle one " \
|
|
131
|
-
"(saved in the session; config toolkit.default; /toolkit changes it)") { |n| options[:toolkit] = n }
|
|
132
|
-
opts.on("--tool NAME", "Also declare this tool on top of the toolkit (repeatable; a comma list too)") { |t| (options[:tools] ||= []) << t }
|
|
133
|
-
# --no-tool is taken out of ARGV before parse! (OptionParser would read it
|
|
134
|
-
# as --tool's negation); this is its help line.
|
|
135
|
-
opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
|
|
136
|
-
opts.on("--set KEY=VALUE", "A setting for this session, saved in it (repeatable): a core key a session may set " \
|
|
137
|
-
"(recap.enabled, recap.sentences; typed by the key, over CLI, env and config.yml), or BUNDLE.KEY over " \
|
|
138
|
-
"config.yml's bundles: (a key the bundle's manifest lists in session_settings:; numbers and booleans " \
|
|
139
|
-
"(true, off, no) are typed, the rest is text). A wrong key or value is refused: " \
|
|
140
|
-
"--set recap.enabled=false, --set cost-cap.cap=3") do |v|
|
|
141
|
-
(options[:sets] ||= []) << v
|
|
142
|
-
end
|
|
143
|
-
opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
|
|
144
|
-
"hard|standard|soft, only stricter than config.yml's guardrails.parent_answers (saved in the " \
|
|
145
|
-
"session; only you loosen it, with /parent-answers in the terminal or the web)") do |v|
|
|
146
|
-
options[:parent_answers] = v
|
|
147
|
-
end
|
|
148
|
-
opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
|
|
149
|
-
opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
|
|
150
|
-
"config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
|
|
151
|
-
opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
|
|
152
|
-
opts.on("--llm-context LAYERS", "This session's own LLM context strategy: none, or layers such as stale,forget (saved in the session, " \
|
|
153
|
-
"before the model's; /llm-context changes it)") { |v| (options[:llm_context] ||= {})[:strategy] = v }
|
|
154
|
-
opts.on("--llm-context-apply RULE", "This session's own apply rule for it: payoff|next_request|turn_end") { |v| (options[:llm_context] ||= {})[:apply] = v }
|
|
155
|
-
opts.on("--llm-context-budget TOKENS", "This session's own context budget: tokens (64000, 64k) or off") { |v| (options[:llm_context] ||= {})[:budget_tokens] = v }
|
|
156
|
-
opts.on("--no-interrupt", "Raise the tool call limit to 1000 iterations (useful for long tasks)") { options[:no_interrupt] = true; cli_overrides["no_interrupt"] = true }
|
|
157
|
-
opts.on("--non-interactive", "Run a single prompt and exit (no REPL, implies --no-interrupt)") { options[:non_interactive] = true }
|
|
158
|
-
opts.on("-v", "--verbose", "Print raw LLM responses and tool calls/results (runs a plain REPL, not attached)") { options[:verbose] = true }
|
|
159
|
-
opts.on("--no-default-input", "Skip default REPL input prefill") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
|
|
160
|
-
opts.on("--port PORT", "Port for `chi web` (default: 4567, env SAMAGOTCHI_WEB_PORT)") { |p| options[:web_port] = p.to_i; cli_overrides["web.port"] = p.to_s }
|
|
161
|
-
opts.on("--open", "Open browser for `chi web`") { options[:web_open] = true }
|
|
162
|
-
opts.on("--new-token", "`chi web`: replace the LAN access token (web.host: lan); links and QR codes made with the old one stop working") { options[:new_token] = true }
|
|
163
|
-
opts.on("--scope VALUE", %w[project all],
|
|
164
|
-
"Which sessions `chi web` and `chi sessions list` show: project (the default: this folder's git project; " \
|
|
165
|
-
"every session outside a repo) or all") { |v| options[:scope] = v }
|
|
166
|
-
# Accepted values of string entries that the registry doesn't type as enums.
|
|
167
|
-
value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
|
|
168
|
-
"web.annotate_presets" => "text|text", "web.editor" => "vscode|vscode-insiders|cursor|zed|none|URL" }
|
|
169
|
-
# Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
|
|
170
|
-
# Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
|
|
171
|
-
Samagotchi::Config.cli_entries.each do |entry|
|
|
172
|
-
next if %w[no_interrupt no_default_input].include?(entry.key)
|
|
173
|
-
next if entry.key == "web.port" # already handled as --port
|
|
174
|
-
next if entry.key == "default.model" # alias --model above
|
|
175
|
-
next if entry.key == "web.markdown" # handled below with explicit inverse flags
|
|
176
|
-
|
|
177
|
-
case entry.type
|
|
178
|
-
when :bool
|
|
179
|
-
# --[no-]: a bool that defaults to on is only useful switched off.
|
|
180
|
-
opts.on("--[no-]#{entry.cli_flag.delete_prefix("--")}", "Set #{entry.key} (bool, default #{entry.default ? "on" : "off"}) [env #{entry.env_key}]") { |v| cli_overrides[entry.key] = v }
|
|
181
|
-
when :integer, :float, :string, :enum
|
|
182
|
-
desc = "Set #{entry.key} [env #{entry.env_key}]"
|
|
183
|
-
desc += " (#{entry.enum_values.join("|")})" if entry.type == :enum
|
|
184
|
-
desc += " (#{value_hints[entry.key]})" if value_hints.key?(entry.key)
|
|
185
|
-
desc += "; only without a hosts: config" if %w[server.host server.port].include?(entry.key)
|
|
186
|
-
if entry.type == :enum
|
|
187
|
-
opts.on("#{entry.cli_flag} VALUE", entry.enum_values, desc) { |v| cli_overrides[entry.key] = v }
|
|
188
|
-
else
|
|
189
|
-
opts.on("#{entry.cli_flag} VALUE", desc) { |v| cli_overrides[entry.key] = v }
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
opts.on("--web-markdown", "Render finalized LLM Markdown in `chi web` (requires optional commonmarker gem)") { options[:web_markdown] = true; cli_overrides["web.markdown"] = true }
|
|
194
|
-
opts.on("--no-web-markdown", "Disable Markdown rendering in `chi web`") { options[:web_markdown] = false; cli_overrides["web.markdown"] = false }
|
|
195
|
-
# Strict underscore rejection: --recap_base_url etc. are not registered and will raise OptionParser::InvalidOption
|
|
196
|
-
opts.on("--version", "Print chi's version") { puts "chi #{Samagotchi::VERSION}"; exit }
|
|
197
|
-
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
198
|
-
end
|
|
199
|
-
# --no-tool NAME (or --no-tool=NAME), up to a "--". A flag that takes a
|
|
200
|
-
# value (-p, --model …: the parser's own list) keeps its next word, so
|
|
201
|
-
# `chi -p "--no-tool=x is broken"` is a prompt. A tool name never starts
|
|
202
|
-
# with "-": `--no-tool --` or `--no-tool --model` is a missing argument.
|
|
203
|
-
switches = parser.instance_variable_get(:@stack).flat_map { |list| list.list }
|
|
204
|
-
.select { |switch| switch.is_a?(OptionParser::Switch) }
|
|
205
|
-
switch_flags = switches.flat_map { |switch| switch.long + switch.short }
|
|
206
|
-
known_flags = (known + switch_flags).uniq
|
|
207
|
-
takes_value = switches.select { |switch| switch.is_a?(OptionParser::Switch::RequiredArgument) }
|
|
208
|
-
.flat_map { |switch| switch.long + switch.short }
|
|
209
|
-
reject_prompt_flag = lambda do |flag, value|
|
|
210
|
-
if known_flags.include?(value)
|
|
211
|
-
warn "Error: #{flag} needs the prompt text right after it (got the flag #{value}) (see chi --help)"
|
|
212
|
-
exit 2
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
index = 0
|
|
216
|
-
while index < ARGV.size && ARGV[index] != "--"
|
|
217
|
-
arg = ARGV[index]
|
|
218
|
-
if %w[-p --prompt].include?(arg)
|
|
219
|
-
reject_prompt_flag.call(arg, ARGV[index + 1])
|
|
220
|
-
index += 2
|
|
221
|
-
elsif arg.start_with?("--prompt=")
|
|
222
|
-
reject_prompt_flag.call("--prompt", arg.delete_prefix("--prompt="))
|
|
223
|
-
index += 1
|
|
224
|
-
elsif arg.start_with?("-p") && arg != "-p"
|
|
225
|
-
reject_prompt_flag.call("-p", arg.delete_prefix("-p").delete_prefix("="))
|
|
226
|
-
index += 1
|
|
227
|
-
else
|
|
228
|
-
index += takes_value.include?(arg) ? 2 : 1
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
missing_no_tool = lambda do
|
|
232
|
-
warn "Error: missing argument: --no-tool (see chi --help)"
|
|
233
|
-
exit 1
|
|
234
|
-
end
|
|
235
|
-
index = 0
|
|
236
|
-
while index < ARGV.size && ARGV[index] != "--"
|
|
237
|
-
arg = ARGV[index]
|
|
238
|
-
if arg == "--no-tool"
|
|
239
|
-
value = ARGV[index + 1]
|
|
240
|
-
missing_no_tool.call if value.nil? || value.start_with?("-")
|
|
241
|
-
(options[:no_tools] ||= []) << ARGV.delete_at(index + 1)
|
|
242
|
-
ARGV.delete_at(index)
|
|
243
|
-
elsif arg.start_with?("--no-tool=")
|
|
244
|
-
value = arg.delete_prefix("--no-tool=")
|
|
245
|
-
missing_no_tool.call if value.empty? || value.start_with?("-")
|
|
246
|
-
options[:no_tools] = (options[:no_tools] || []) << value
|
|
247
|
-
ARGV.delete_at(index)
|
|
248
|
-
else
|
|
249
|
-
index += takes_value.include?(arg) ? 2 : 1
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
begin
|
|
253
|
-
parser.parse!
|
|
254
|
-
rescue OptionParser::ParseError => e
|
|
255
|
-
warn "Error: #{e.message} (see chi --help)"
|
|
256
|
-
exit 1
|
|
257
|
-
end
|
|
103
|
+
# The run flags (RunOptions: the parser and its checks); what is left in
|
|
104
|
+
# ARGV is `web` or a stray word.
|
|
105
|
+
require "samagotchi/run_options"
|
|
106
|
+
options, cli_overrides = Samagotchi::RunOptions.parse(ARGV)
|
|
258
107
|
|
|
259
108
|
# The subcommands with their own parsers ran above; what parse! leaves is
|
|
260
109
|
# `web` or nothing. Anything else is a typo, not a prompt: refuse it rather
|
|
@@ -313,6 +162,29 @@ Samagotchi::Config.reload!(cli_overrides: cli_overrides) unless cli_overrides.em
|
|
|
313
162
|
require "samagotchi/log"
|
|
314
163
|
Samagotchi::Log.configure(level: options[:verbose] ? :debug : nil, mirror: options[:verbose])
|
|
315
164
|
|
|
165
|
+
# chi --remote NAME --attach ID (--resume ID, --dir DIR, -p TEXT): the
|
|
166
|
+
# attached TUI on that machine's session. Nothing below runs: it checks
|
|
167
|
+
# this machine's sessions, memories and files.
|
|
168
|
+
if remote_attach
|
|
169
|
+
unless ARGV.empty?
|
|
170
|
+
warn "Error: unknown command #{ARGV.first} (see chi --help)"
|
|
171
|
+
exit 1
|
|
172
|
+
end
|
|
173
|
+
require "samagotchi/remote_attach_command"
|
|
174
|
+
begin
|
|
175
|
+
entry = Samagotchi::Remotes.find(Samagotchi::Remotes.check_name!(remote_run.remote))
|
|
176
|
+
rescue Samagotchi::Remotes::Invalid => e
|
|
177
|
+
warn "Error: #{e.message}"
|
|
178
|
+
exit 2
|
|
179
|
+
end
|
|
180
|
+
exit Samagotchi::RemoteAttachCommand.new(options, access: Samagotchi::SessionAccess::Remote.new(entry)).run
|
|
181
|
+
end
|
|
182
|
+
# --dir names a folder on a remote; here the session starts in this one.
|
|
183
|
+
if options[:dir]
|
|
184
|
+
warn "Error: --dir goes with --remote; cd there instead"
|
|
185
|
+
exit 2
|
|
186
|
+
end
|
|
187
|
+
|
|
316
188
|
# `chi web`: its flags (--port, --open, --web-view…) went through the parser above.
|
|
317
189
|
options[:web] = true if web
|
|
318
190
|
|
|
@@ -360,7 +232,8 @@ end
|
|
|
360
232
|
end
|
|
361
233
|
next unless leftover
|
|
362
234
|
|
|
363
|
-
|
|
235
|
+
require "samagotchi/launch_mode"
|
|
236
|
+
warn "Error: #{Samagotchi::LaunchMode::SCRATCH_LEFTOVER}"
|
|
364
237
|
exit 1
|
|
365
238
|
end
|
|
366
239
|
|
|
@@ -476,15 +349,7 @@ launch, launch_note = if scratch
|
|
|
476
349
|
warn launch_note if launch_note
|
|
477
350
|
|
|
478
351
|
if launch == :attached
|
|
479
|
-
|
|
480
|
-
.find { |key, _flag| options[key] }
|
|
481
|
-
error = if options[:attach] && options[:shared]
|
|
482
|
-
"use either --attach ID or --shared"
|
|
483
|
-
elsif options[:attach] && options[:resume]
|
|
484
|
-
"--attach takes the session id; --resume goes with --shared"
|
|
485
|
-
elsif conflict
|
|
486
|
-
"--attach/--shared can't be combined with #{conflict.last}"
|
|
487
|
-
end
|
|
352
|
+
error = Samagotchi::LaunchMode.attached_conflict(options)
|
|
488
353
|
if error
|
|
489
354
|
warn "Error: #{error}"
|
|
490
355
|
exit 1
|
|
@@ -508,6 +373,10 @@ if launch == :attached
|
|
|
508
373
|
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
509
374
|
warn "Error: #{e.message}"
|
|
510
375
|
exit 1
|
|
376
|
+
rescue Interrupt
|
|
377
|
+
# Ctrl-C while it connects (a wait for a leaving worker, a late Bridge):
|
|
378
|
+
# no backtrace.
|
|
379
|
+
exit Samagotchi::CLI::Exit::INTERRUPTED
|
|
511
380
|
end
|
|
512
381
|
exit Samagotchi::TerminalUI::AttachLauncher.exit_status(ended)
|
|
513
382
|
end
|
data/docs/architecture.md
CHANGED
|
@@ -125,6 +125,11 @@ terminal renders the events it gets over the Bridge.
|
|
|
125
125
|
- `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`;
|
|
126
126
|
`web.host: lan` adds a LAN address, `--new-token` replaces its access token).
|
|
127
127
|
- `bin/chi sessions {list,stop,restart,archive,unarchive,delete,prune,clean,stats}` (`SessionsCommand`; not the REPL's `SessionCommands`) → retention & ordering (`SessionRetention`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
|
|
128
|
+
- Session commands (`chi send`, `answer`, `note`, a session's `context`, `sessions list/stop/restart/archive/unarchive/delete`) →
|
|
129
|
+
`SessionAccess` (`session_access.rb`): what they ask of a session goes through one backend object,
|
|
130
|
+
`SessionAccess::Local` (`session_access/local.rb`) on this machine's files and workers; a `--wait`'s pre-send state
|
|
131
|
+
(cursor, baseline, own turn) is `prepare_wait`'s `WaitStart`. `SessionAccess::Contract` lists which commands and flags
|
|
132
|
+
run against a remote and which on this machine only.
|
|
128
133
|
- In REPL mode `TerminalUI` builds the `Engine` in process (`-p` and `--non-interactive` too);
|
|
129
134
|
otherwise the session's `Worker` (`lib/samagotchi/worker.rb`, spawned by `SessionManager`) builds it.
|
|
130
135
|
|
|
@@ -464,7 +469,8 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
|
|
|
464
469
|
The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
|
|
465
470
|
for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
|
|
466
471
|
(503 `not_live` when the worker is not running; full history of any session is served by
|
|
467
|
-
`GET /api/sessions/:id/output`)
|
|
472
|
+
`GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
|
|
473
|
+
request principal's frame filter (`App#stream_filter`, nil for the owner, the only principal today). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
|
|
468
474
|
an earlier worker gets the `reset` snapshot, not a replay.
|
|
469
475
|
|
|
470
476
|
**Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
|
|
@@ -499,6 +505,65 @@ names its `chi_version` too.
|
|
|
499
505
|
`GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
|
|
500
506
|
a hub; an `App.new` without one answers both routes `503 no_hub`.
|
|
501
507
|
|
|
508
|
+
**chi web's routes for the session commands.** chi's session commands (`chi send`, `answer`, `note`, a session's
|
|
509
|
+
`context`, `sessions list/stop/restart/archive/unarchive/delete`) run against another machine's sessions through its
|
|
510
|
+
chi web (the remote-sessions plan; `SessionAccess::Contract` says which commands and flags may). The page's routes
|
|
511
|
+
cover most of it; `Web::SessionAccessRoutes` (`web/session_access_routes.rb`, in `App`'s `ROUTES`) adds the rest, each
|
|
512
|
+
calling the `SessionAccess::Local` operation of the same name, so a remote backend maps 1:1 onto them. Every one is
|
|
513
|
+
token-gated like the rest (LAN mode: `Authorization: Bearer`), and one that takes an id is a `:session` route (an id
|
|
514
|
+
that isn't one, or no such session: 404 `not_found`). Every `ROUTES` row also names who may use it, checked against
|
|
515
|
+
the request's `Web::Principal` (`App#principal_for`): `owner`, or `participant` for a route a participant may use too.
|
|
516
|
+
A participant (a `Web::Grants` token for one session) may use three: `GET /`, `GET /api/info` and `GET /api/sessions/:id`,
|
|
517
|
+
each answered as theirs; every other route is `owner` (Permission below, and the participants routes in
|
|
518
|
+
[internals/participants.md](internals/participants.md)); `spec/web/principal_spec.rb` fails on a row without one. How a request flows end to end, which checks run where and the trust
|
|
519
|
+
model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
520
|
+
|
|
521
|
+
| Route | Permission | Body / params | Answer | Refusals |
|
|
522
|
+
|---|---|---|---|---|
|
|
523
|
+
| `GET /api/sessions/resolve?id=` | owner | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
|
|
524
|
+
| `GET /api/sessions/summaries` | owner | `dir` (a folder: its project; none: every project), `cwd`, `live=1`, `archived=1`, `tests=0`, `sort`, `order`, `limit` | 200: the rows `chi sessions list` prints (`SessionManager.session_summaries`: `status`, `live`, `busy`, `owner`, `recap`, `ctx_pct`, `waiting`, `waiting_id`, `title`, `preview`, …) | 400 `invalid_dir`, `invalid_cwd` |
|
|
525
|
+
| `POST /api/sessions` (more fields) | owner | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning` and `parent_answers_warning` (a level looser than config.yml's, capped) | 400 `invalid_toolkit`, `invalid_parent_answers`, `invalid_continues` (beside `continues`); 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
|
|
526
|
+
| `POST /api/sessions/:id/turn` (more fields) | owner | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
|
|
527
|
+
| `POST /api/sessions/:id/wait/prepare` | owner | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
|
|
528
|
+
| `POST /api/sessions/:id/wait` | owner | `cursor`, `baseline`, `turn`, `queued`, `ended_before`, `owner_grace`, `timeout` (capped at 30 s) | 200: `ReplyWait::Result`'s fields (`status`: `done`, `waiting_for_answer`, `no_reply`, `error`, `stopped`, `worker_gone`, or `timeout`: ask again; `text`, `file`, `question` as `PendingQuestion#to_file`, `outcome`, `limit`, `error_kind`, `retryable`, `cancel_reason`, `stopped_by`, `kept_steps`, `turn_id`, `origin`; never the session) and `parent_strategy` `{name, allowed, origin}` (`ParentReport.strategy_for`, this server's config.yml) | 400 `invalid_wait`; 503 `server_stopping` |
|
|
529
|
+
| `POST /api/sessions/:id/notes` | owner | `text`, `source` (default `cli`, never `broadcast`) | 200 `{status: queued \| waits, session_id, queued}` (`NoteDelivery`) | 409 `owned_by_tui` (`status: refused`); 400 `note_rejected`, `missing_fields`, `invalid_source` |
|
|
530
|
+
| `POST /api/sessions/:id/context` (more bodies) | owner | `{name, push: true, why, hint}` or `{name, cmd, every, why, hint}` beside the page's `{url, why, hint}`; `added_by` `cli`/`agent` (else `web`) | 201 `{status: attached, name}` | 400 `invalid_source`; 409 `exists` |
|
|
531
|
+
| `POST /api/sessions/:id/context/:name/push` | owner | `text`, `summary`, `wake`, `hint` (a source's JSON output, checked the same way) | 200 `{status: pushed, name, before, revision, changed}` | 404 `not_found`; 409 `not_push`; 400 `invalid_text` |
|
|
532
|
+
| `DELETE /api/sessions/:id/context/:name?own=1` | owner | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
|
|
533
|
+
| `POST /api/sessions/:id/stop` | owner | `wait?` (seconds, capped at `SessionManager::STOP_WAIT`, 10: `chi --remote NAME sessions stop` sends it) | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within the wait (without one `STOP_WAIT_SECONDS`, 2 s: the page's Stop) | 409 `owned_by_tui` |
|
|
534
|
+
| `DELETE /api/sessions/:id?force=` | owner | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
|
|
535
|
+
| `GET /api/sessions/:id?brief=1` | participant (theirs: title, status, participants) | | 200 `{brief, session: {id, status (the file's), title, display_title, project_root, working_directory, model_name, model_typed, parent_answers, owner}, pending_question}` (the file's; no messages): `Remote#show` | 404 |
|
|
536
|
+
| `POST /api/sessions/:id/turn` (its 202) | owner | | the ack with `via`: `bridge` or `file` (the input file: a remote `chi send --wait` follows that turn) | |
|
|
537
|
+
| `GET /api/sessions/:id/context?full=1`, `…/context/:name?full=1` | owner | | each row with `source` (`Source#to_h`, its command too; the page's rows never carry one), `snapshot` (`Snapshot#to_h`, no text) and `shadowed`, shadowed project sources included | |
|
|
538
|
+
| `POST /api/sessions/:id/wake` | owner | `wait` (seconds, default and most 10: `WAKE_MAX_WAIT`) | 200 `{session_id, woke, worker: {epoch, started_at, version, features}, model_name, default_model, parent_strategy, installed}`: the live worker as is (`woke: false`), else resumed and its Bridge waited for (`Local#wake`); `default_model` and `installed` are this machine's | 409 `owned_by_tui`, `scratch` (`LaunchMode::SCRATCH_LEFTOVER`); 504 `worker_timeout` (`LaunchMode::BRIDGE_LATE`); 400 `invalid_wait`; 404 |
|
|
539
|
+
| `GET /api/sessions/:id/worker` | owner | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, default_model, parent_strategy}` (`Local#worker`; never starts one; `default_model` is this machine's) | 404 |
|
|
540
|
+
| `POST /api/sessions/:id/exit` | owner | `client_id` (passed on only when it is an attached TUI's, `tui:…`; any other is dropped), `delete` | 200 the worker's `{status: exiting, session_id, discard}` (`BridgeClient#request_exit`; never wakes one) | 409 `held` with the worker's `{status, reason, tasks, hint}`; 503 `not_live`; 501 `not_supported`; 504 `worker_timeout` |
|
|
541
|
+
| `POST /api/sessions/:id/recap` | owner | | 200 the worker's `{enabled, saved, request, min_user_turns, session_off}` (`BridgeClient#request_recap`) | 503, 501, 504 as every relay |
|
|
542
|
+
| `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_error` |
|
|
543
|
+
| `GET /api/sessions/:id/children/counts` | owner | | 200 `{running, waiting, unreported}` (`ChildrenStatus.counts`, the delegates' files here) | 404 |
|
|
544
|
+
| `GET /api/sessions/:id/turns/:turn_id/records` | owner | | 200 `{tool_records: […], turn_record: {…} \| null}` (`SessionMetrics.saved_tool_records` / `saved_turn_record`, as saved) | 400 `invalid_turn_id`; 404 |
|
|
545
|
+
| `POST /api/sessions/:id/turn` (`no_interrupt`) | owner | `no_interrupt: true`: the turn runs with the raised iteration limit (`SessionManager.deliver_turn(no_interrupt:)`, to the Bridge or the input file) | as before | |
|
|
546
|
+
| `POST /api/sessions/:id/command` (its 501) | owner | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
|
|
547
|
+
| `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404 |
|
|
548
|
+
|
|
549
|
+
`GET /api/info` names the feature `session_commands` (`SessionAccessRoutes::FEATURE`): `SessionAccess::Remote` asks it
|
|
550
|
+
once before its first request, and a chi web without it (or `serve_static`'s 404 for a path no route takes) is "remote NAME runs chi X, too
|
|
551
|
+
old for this command through --remote: update chi there (chi update)". The feature `remote_attach`
|
|
552
|
+
(`SessionAccessRoutes::ATTACH_FEATURE`) names the routes a remote attached TUI needs: wake, worker, exit, recap, stats,
|
|
553
|
+
children counts and a turn's records. Their refusals carry `{error, detail, message}`.
|
|
554
|
+
|
|
555
|
+
`POST …/answer` passes on `client_id` only when it is chi answer's (`cli:answer`), so the worker holds the answer to
|
|
556
|
+
its `guardrails.parent_answers`; any other is dropped, so a forwarded id can only tighten. The worker's 403
|
|
557
|
+
`parent_answer_refused` (with `reason`) and a dismissal's 409 `not_dismissable` pass through; a 408 (the worker read it
|
|
558
|
+
past its deadline) is sent once more, as `SessionAccess::Local#answer` does, and a second is `504 worker_timeout`.
|
|
559
|
+
|
|
560
|
+
A refusal an exception raises carries, next to `error` and `detail` (the page's words), `message` (the exception's own,
|
|
561
|
+
verbatim) and the fields its class is built from, so a remote client raises the same class with the same text:
|
|
562
|
+
`owned_by_tui` (`session_id`), `worker_running`/`still_stopping` (`DeleteRefused`: `session_id`, `reason`), `busy`/
|
|
563
|
+
`scratch` (`ArchiveRefused`: `session_id`, `reason`, `busy_id`), a restart's `held`/`not_running`/`unsupported`/
|
|
564
|
+
`failed` (`RestartRefused`: `session_id`, `reason`, `version`), a continue's `continued`/`open_children`/`folder_gone`
|
|
565
|
+
(`SessionContinue::Refused`: `session_id`, `reason`, `ids`) and `ambiguous_id` (`Session::AmbiguousId`: `ids`).
|
|
566
|
+
|
|
502
567
|
## Model loops and adapters
|
|
503
568
|
|
|
504
569
|
A model ref (`--model`, `/model`, `default.model`, an alias's target) is parsed
|
|
@@ -619,6 +684,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
619
684
|
| Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
|
|
620
685
|
| Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
|
|
621
686
|
| send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
|
|
687
|
+
| Session access | `session_access.rb` (`SessionAccess`: `NewSession`, `SetupWords`, `WaitStart`, `SessionView`, `Created`, `ContextEntry`, `ContextPush`, the errors `RemoteFailed`, `RemoteTooOld`, `Refused`, `BadRequest`), `session_access/local.rb` (`SessionAccess::Local`), `session_access/remote.rb` (`SessionAccess::Remote`), `session_access/http_transport.rb`, `session_access/web_bridge_client.rb` (`SessionAccess::WebBridgeClient`, a remote attach's worker through chi web), `session_access/dispatch.rb` (`SessionAccess::Dispatch`), `session_access/contract.rb` (`SessionAccess::Contract`), `remotes.rb` (`Remotes`), `remote_command.rb` (`chi remote`), `web/session_access_routes.rb` (`Web::SessionAccessRoutes`) | The seam between the session commands and where the sessions live: `Local` is this machine's (the `SessionManager`, `ReplyWait`, `NoteDelivery`, `ContextSources` and Bridge calls the commands made); `Remote` is another machine's, the same methods as requests to its chi web's routes of the same name (Bearer token, `HTTPTransport`), results as Local's value objects and refusals as the classes Local's callees raise, built from the fields chi web sends; waits and prefixes run on the server, a wait asked again per 30 s slice. `Dispatch` is bin/chi's front for `--remote NAME` / `CHI_REMOTE` (taken off before the subcommand lookup): it refuses what the contract table lists `local_only` (exit 2, one message) and runs the command with a `Remote` on the `Remotes` entry (`$XDG_CONFIG_HOME/samagotchi/remotes.yml`, 0600). chi web's routes answer from `Local` (*chi web's routes for the session commands* above). Checks that read this machine's config or files (vision, parent answers, the cwd's project, `SAMAGOTCHI_PARENT_SESSION`) stay in the commands and are skipped under a `Remote`, where chi web's create, turn and wait routes run the vision, parent-answers and busy checks with the server's config and the worker checks a parent's answer. See [internals/remote-sessions.md](internals/remote-sessions.md). |
|
|
622
688
|
| Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
|
|
623
689
|
| Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
|
|
624
690
|
| LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
|