samagotchi 0.102.0 → 0.103.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 +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- 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 +43 -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/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -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 +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- 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/session_access_routes.rb +343 -0
- 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 +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c4a19357a9f5c94b3c76052abc018a034968ad8baf3520d50ace0ab581943b6
|
|
4
|
+
data.tar.gz: ec637848fd8dc33b292d836618753699bc8159962c6051d7b9b669beb31eb50f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ca1a63b9c68e9b18b21fb2f2bf6503eb3cec3368e10410c4efce78dc5a853e842fc8ddd74bb5387d21f6b0d38c69f4abdb58a5320bf5c9e8272232ebf56234c
|
|
7
|
+
data.tar.gz: 7c86172b9193fc68c5c639989573c17f99af9621d118624684ace1483628a01cde8e609c2438368c297e534043fb86a165e94511e34dfd880171d77f2a75eb57
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,44 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.103.0] - 2026-10-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Remote sessions: `chi --remote NAME send|answer|note|context|sessions …` (or `CHI_REMOTE=NAME`) runs the session
|
|
16
|
+
commands against another machine's sessions, through its `chi web --web-host lan`. `chi remote add NAME URL
|
|
17
|
+
--token-file FILE` checks the address and token and saves the remote (`chi remote ls`, `chi remote rm`).
|
|
18
|
+
`send --new` there needs `--dir`, a folder on that machine; what runs on this machine only (attach, `sessions
|
|
19
|
+
stats`, `context mute`, `broadcast`, …) is refused with exit 2. The next-step commands it prints (`chi answer …`,
|
|
20
|
+
`chi send --wait …`) carry `--remote NAME`; where chi points at `chi --attach` here it names that machine's chi web.
|
|
21
|
+
See [docs/cli.md](docs/cli.md#remote-sessions).
|
|
22
|
+
- `/working-set pin KEY…` keeps an item from the model's `forget_outputs`: its reads, those to come too, stay
|
|
23
|
+
whole until `/working-set unpin KEY…`. `/working-set add KEY…` brings an item in: the next turn's line asks the
|
|
24
|
+
model to read it first (one tool call; what it reads is an ordinary read); `/working-set unadd KEY…` takes an
|
|
25
|
+
add back before then. In the web's working set list (still experimental, `web.working_set: true`), rows offer
|
|
26
|
+
`pin` / `unpin`, `+` adds an item, and a stale or forgotten row offers a refresh. See [docs/cli.md](docs/cli.md).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The guardrails bundle (0.9.3) asks for `chi --remote NAME context add --cmd …` and `chi --remote NAME broadcast`
|
|
31
|
+
as it does without `--remote`. Update with `chi update`.
|
|
32
|
+
- claude-peer (0.4.2): `pair` through a remote (`CHI_REMOTE`, or `--remote NAME` in `CHI`) attaches only
|
|
33
|
+
`claude-outbox` and says chi there can't send to that Claude. Update with `chi update`.
|
|
34
|
+
- codex-peer (0.3.3): the same for `pair` through a remote: it attaches only `codex-outbox`, says chi there can't
|
|
35
|
+
send to that Codex, and when chi can't reach the remote names Codex's sandbox `network_access` setting as a
|
|
36
|
+
possible cause. Update with `chi update`.
|
|
37
|
+
- chi web's `POST /api/sessions/:id/stop` also answers `released`; `DELETE /api/sessions/:id` takes `force` (the
|
|
38
|
+
page sends it). Both are additive.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- The web's stage shrinks back when you close its steps list by hand, instead of keeping its grown height.
|
|
43
|
+
- `/working-set`: a `memory_read` of several names is dropped, pinned or added as a whole (the reply names the
|
|
44
|
+
others); a quoted KEY keeps its spaces, in the TUI and the web; a file in the session's folder is shown by its
|
|
45
|
+
relative path.
|
|
46
|
+
- The idle recap's summarize thread ends quietly, logged, on a failure.
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
11
49
|
## [0.102.0] - 2026-10-23
|
|
12
50
|
|
|
13
51
|
### Added
|
|
@@ -3854,7 +3892,8 @@ and long-lived sessions.
|
|
|
3854
3892
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3855
3893
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3856
3894
|
|
|
3857
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3895
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.103.0...HEAD
|
|
3896
|
+
[0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
|
|
3858
3897
|
[0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
|
|
3859
3898
|
[0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
|
|
3860
3899
|
[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
|
|
@@ -64,8 +63,23 @@ SUBCOMMANDS = {
|
|
|
64
63
|
# list and remove memories (ls, rm; --scope, --format)
|
|
65
64
|
"memory" => ["samagotchi/memory_command", "MemoryCommand"],
|
|
66
65
|
# install, upgrade, uninstall, status, diff, list, build (--scope, --force, …)
|
|
67
|
-
"bundle" => ["samagotchi/bundle_command", "BundleCommand"]
|
|
66
|
+
"bundle" => ["samagotchi/bundle_command", "BundleCommand"],
|
|
67
|
+
# other machines' chi web, for chi --remote NAME (add, ls, rm)
|
|
68
|
+
"remote" => ["samagotchi/remote_command", "RemoteCommand"]
|
|
68
69
|
}.freeze
|
|
70
|
+
# chi --remote NAME <command> (or CHI_REMOTE=NAME): the session commands
|
|
71
|
+
# against another machine's chi web; what runs here only is refused (exit 2).
|
|
72
|
+
# chi remote, --version and --help run here as always.
|
|
73
|
+
require "samagotchi/session_access/dispatch"
|
|
74
|
+
begin
|
|
75
|
+
remote_run = Samagotchi::SessionAccess::Dispatch.from(ARGV)
|
|
76
|
+
rescue Samagotchi::SessionAccess::Dispatch::Usage => e
|
|
77
|
+
warn "Error: #{e.message}"
|
|
78
|
+
exit 2
|
|
79
|
+
end
|
|
80
|
+
ARGV.replace(remote_run.argv)
|
|
81
|
+
exit remote_run.run(SUBCOMMANDS) if remote_run.remote? && !remote_run.here?
|
|
82
|
+
|
|
69
83
|
if (path, klass = SUBCOMMANDS[ARGV[0]])
|
|
70
84
|
require path
|
|
71
85
|
exit Samagotchi.const_get(klass).new(ARGV[1..] || []).run
|
|
@@ -76,185 +90,10 @@ end
|
|
|
76
90
|
scratch = ARGV[0] == "scratch"
|
|
77
91
|
ARGV.shift if scratch
|
|
78
92
|
|
|
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
|
|
93
|
+
# The run flags (RunOptions: the parser and its checks); what is left in
|
|
94
|
+
# ARGV is `web` or a stray word.
|
|
95
|
+
require "samagotchi/run_options"
|
|
96
|
+
options, cli_overrides = Samagotchi::RunOptions.parse(ARGV)
|
|
258
97
|
|
|
259
98
|
# The subcommands with their own parsers ran above; what parse! leaves is
|
|
260
99
|
# `web` or nothing. Anything else is a typo, not a prompt: refuse it rather
|
|
@@ -360,7 +199,8 @@ end
|
|
|
360
199
|
end
|
|
361
200
|
next unless leftover
|
|
362
201
|
|
|
363
|
-
|
|
202
|
+
require "samagotchi/launch_mode"
|
|
203
|
+
warn "Error: #{Samagotchi::LaunchMode::SCRATCH_LEFTOVER}"
|
|
364
204
|
exit 1
|
|
365
205
|
end
|
|
366
206
|
|
|
@@ -476,15 +316,7 @@ launch, launch_note = if scratch
|
|
|
476
316
|
warn launch_note if launch_note
|
|
477
317
|
|
|
478
318
|
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
|
|
319
|
+
error = Samagotchi::LaunchMode.attached_conflict(options)
|
|
488
320
|
if error
|
|
489
321
|
warn "Error: #{error}"
|
|
490
322
|
exit 1
|
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
|
|
|
@@ -499,6 +504,54 @@ names its `chi_version` too.
|
|
|
499
504
|
`GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
|
|
500
505
|
a hub; an `App.new` without one answers both routes `503 no_hub`.
|
|
501
506
|
|
|
507
|
+
**chi web's routes for the session commands.** chi's session commands (`chi send`, `answer`, `note`, a session's
|
|
508
|
+
`context`, `sessions list/stop/restart/archive/unarchive/delete`) run against another machine's sessions through its
|
|
509
|
+
chi web (the remote-sessions plan; `SessionAccess::Contract` says which commands and flags may). The page's routes
|
|
510
|
+
cover most of it; `Web::SessionAccessRoutes` (`web/session_access_routes.rb`, in `App`'s `ROUTES`) adds the rest, each
|
|
511
|
+
calling the `SessionAccess::Local` operation of the same name, so a remote backend maps 1:1 onto them. Every one is
|
|
512
|
+
token-gated like the rest (LAN mode: `Authorization: Bearer`), and one that takes an id is a `:session` route (an id
|
|
513
|
+
that isn't one, or no such session: 404 `not_found`). How a request flows end to end, which checks run where and the trust
|
|
514
|
+
model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
515
|
+
|
|
516
|
+
| Route | Body / params | Answer | Refusals |
|
|
517
|
+
|---|---|---|---|
|
|
518
|
+
| `GET /api/sessions/resolve?id=` | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
|
|
519
|
+
| `GET /api/sessions/summaries` | `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` |
|
|
520
|
+
| `POST /api/sessions` (more fields) | `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` |
|
|
521
|
+
| `POST /api/sessions/:id/turn` (more fields) | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
|
|
522
|
+
| `POST /api/sessions/:id/wait/prepare` | `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`) |
|
|
523
|
+
| `POST /api/sessions/:id/wait` | `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` |
|
|
524
|
+
| `POST /api/sessions/:id/notes` | `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` |
|
|
525
|
+
| `POST /api/sessions/:id/context` (more bodies) | `{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` |
|
|
526
|
+
| `POST /api/sessions/:id/context/:name/push` | `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` |
|
|
527
|
+
| `DELETE /api/sessions/:id/context/:name?own=1` | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
|
|
528
|
+
| `POST /api/sessions/:id/stop` | | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within `STOP_WAIT_SECONDS` (2 s) | 409 `owned_by_tui` |
|
|
529
|
+
| `DELETE /api/sessions/:id?force=` | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
|
|
530
|
+
| `GET /api/sessions/:id?brief=1` | | 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 |
|
|
531
|
+
| `POST /api/sessions/:id/turn` (its 202) | | the ack with `via`: `bridge` or `file` (the input file: a remote `chi send --wait` follows that turn) | |
|
|
532
|
+
| `GET /api/sessions/:id/context?full=1`, `…/context/:name?full=1` | | 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 | |
|
|
533
|
+
| `POST /api/sessions/:id/wake` | `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 |
|
|
534
|
+
| `GET /api/sessions/:id/worker` | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, parent_strategy}` (`Local#worker`; never starts one) | 404 |
|
|
535
|
+
| `POST /api/sessions/:id/turn` (`no_interrupt`) | `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 | |
|
|
536
|
+
| `POST /api/sessions/:id/command` (its 501) | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
|
|
537
|
+
|
|
538
|
+
`GET /api/info` names the feature `session_commands` (`SessionAccessRoutes::FEATURE`): `SessionAccess::Remote` asks it
|
|
539
|
+
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
|
|
540
|
+
old for this command through --remote: update chi there (chi update)". The feature `remote_attach`
|
|
541
|
+
(`SessionAccessRoutes::ATTACH_FEATURE`) names the wake and worker routes a remote attached TUI needs.
|
|
542
|
+
|
|
543
|
+
`POST …/answer` passes on `client_id` only when it is chi answer's (`cli:answer`), so the worker holds the answer to
|
|
544
|
+
its `guardrails.parent_answers`; any other is dropped, so a forwarded id can only tighten. The worker's 403
|
|
545
|
+
`parent_answer_refused` (with `reason`) and a dismissal's 409 `not_dismissable` pass through; a 408 (the worker read it
|
|
546
|
+
past its deadline) is sent once more, as `SessionAccess::Local#answer` does, and a second is `504 worker_timeout`.
|
|
547
|
+
|
|
548
|
+
A refusal an exception raises carries, next to `error` and `detail` (the page's words), `message` (the exception's own,
|
|
549
|
+
verbatim) and the fields its class is built from, so a remote client raises the same class with the same text:
|
|
550
|
+
`owned_by_tui` (`session_id`), `worker_running`/`still_stopping` (`DeleteRefused`: `session_id`, `reason`), `busy`/
|
|
551
|
+
`scratch` (`ArchiveRefused`: `session_id`, `reason`, `busy_id`), a restart's `held`/`not_running`/`unsupported`/
|
|
552
|
+
`failed` (`RestartRefused`: `session_id`, `reason`, `version`), a continue's `continued`/`open_children`/`folder_gone`
|
|
553
|
+
(`SessionContinue::Refused`: `session_id`, `reason`, `ids`) and `ambiguous_id` (`Session::AmbiguousId`: `ids`).
|
|
554
|
+
|
|
502
555
|
## Model loops and adapters
|
|
503
556
|
|
|
504
557
|
A model ref (`--model`, `/model`, `default.model`, an alias's target) is parsed
|
|
@@ -619,6 +672,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
619
672
|
| 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
673
|
| 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
674
|
| 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)). |
|
|
675
|
+
| 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
676
|
| Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
|
|
623
677
|
| 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
678
|
| 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)). |
|
data/docs/cli.md
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
22
22
|
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
23
23
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
|
|
24
|
+
- `chi remote add NAME URL [--token-file F | --token-stdin]`, `chi remote ls`, `chi remote rm NAME` — the other machines whose chi web the session commands reach with `--remote NAME` (see [Remote sessions](#remote-sessions))
|
|
25
|
+
- `chi --remote NAME send|answer|note|context|sessions …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
|
|
24
26
|
- `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1 (see [Memory: Listing and removing from the shell](memory.md#listing-and-removing-from-the-shell))
|
|
25
27
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
26
28
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
@@ -278,6 +280,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
|
|
|
278
280
|
| `--shared` | Run the session (new, or `--resume`'s) in a background worker and attach to it: the default, and the way to get it when `session.shared` is off. See [Sharing a session](#sharing-a-session). |
|
|
279
281
|
| `--no-shared` | Run the plain in-process REPL for this run. |
|
|
280
282
|
| `--attach SESSION_ID` | Attach to a session's worker, waking one if it has exited. |
|
|
283
|
+
| `--remote NAME` | Before a session command (`chi --remote studio send …`): run it against that remote's sessions, through its chi web; beats `CHI_REMOTE`. Commands and flags that run on this machine only exit 2. See [Remote sessions](#remote-sessions). |
|
|
281
284
|
| `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
|
|
282
285
|
| `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
|
|
283
286
|
| `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
|
|
@@ -611,16 +614,39 @@ KEY…` takes an item out of what the model is sent: every read of it still sent
|
|
|
611
614
|
goes as a stub, `(dropped by the user) — read it again if the task needs it`,
|
|
612
615
|
whatever the LLM context strategy (none too), and stays dropped across a
|
|
613
616
|
strategy switch. KEY is a path (absolute or the session folder's), `memory:NAME`
|
|
614
|
-
or a URL. `/working-set undrop KEY…` sends it whole again. A change applies
|
|
617
|
+
or a URL; quote one with spaces (`"my notes/a b.md"`, as the listing shows it). `/working-set undrop KEY…` sends it whole again. A change applies
|
|
615
618
|
from the next turn's first request, which also carries one line telling the
|
|
616
619
|
model what you dropped or brought back (so it doesn't read it again by reflex);
|
|
617
620
|
the reply says what it frees and the tail the server reads again (a drop changes
|
|
618
621
|
history, so the prompt cache breaks there once). An undrop before that request
|
|
619
622
|
undoes the drop for free. Refused, with why: an item only edited or written
|
|
620
623
|
(nothing of it to drop), one not in the set, an older session's output chi
|
|
621
|
-
can't name. The model can't undo a drop (`forget_outputs` refuses it).
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
can't name. The model can't undo a drop (`forget_outputs` refuses it). One
|
|
625
|
+
`memory_read` of several names is one output: dropping, undropping or pinning
|
|
626
|
+
one name acts on that read whole, so on the others too, and the reply names
|
|
627
|
+
them (`; the same memory_read holds memory tone: dropped with it`).
|
|
628
|
+
`/working-set pin KEY…` keeps an item from the model's `forget_outputs` (the
|
|
629
|
+
[forget layer](configuration.md#llm-context-the-forget-layer)): it refuses every
|
|
630
|
+
read of it, those to come too, until `/working-set unpin KEY…`. A pin sends
|
|
631
|
+
nothing different (no cache break) and is saved with the session, so a respawn,
|
|
632
|
+
a continue and a fork keep it, across a strategy switch too; under a strategy
|
|
633
|
+
without forget it waits until one has it (the reply says so). The listing marks
|
|
634
|
+
it `pinned`, and under forget the next turn's line tells the model. A pinned
|
|
635
|
+
item can't be dropped until you unpin it; a pin takes back a drop not yet sent
|
|
636
|
+
(free), and asks you to undrop one already sent first.
|
|
637
|
+
`/working-set add KEY…` brings an item in: the next turn's line asks the
|
|
638
|
+
model to load it before anything else (`read` for a file, `memory_read` for
|
|
639
|
+
`memory:NAME`, `web_fetch` for an http(s) URL), so it costs the model one tool
|
|
640
|
+
call, and what it reads is an ordinary read (droppable, forgettable, in the
|
|
641
|
+
set). Nothing is sent until then, and `/working-set unadd KEY…` takes an add
|
|
642
|
+
back for free. On a stale or forgotten item it is a refresh (the model reads
|
|
643
|
+
it again). Refused, with why: a file that isn't there (or a directory), a
|
|
644
|
+
memory neither scope holds, a URL that isn't http(s), an item the model is
|
|
645
|
+
sent whole now, a dropped one (undrop it instead), one already added. The
|
|
646
|
+
listing's last line names the pending adds (`added lib/tax.rb`). The model is
|
|
647
|
+
asked, not forced: a model may skip the read.
|
|
648
|
+
`/working-set` alone answers mid-turn too; `drop`/`undrop`/`pin`/`unpin`/`add`/`unadd`
|
|
649
|
+
sent while a turn runs wait for the turn's end.
|
|
624
650
|
|
|
625
651
|
`/thinking` shows the thinking level the next turn runs at and where it came
|
|
626
652
|
from (`session`, `--thinking` / `SAMAGOTCHI_THINKING_LEVEL`, `models: <key>`,
|
|
@@ -1009,13 +1035,26 @@ turn view ignores it.
|
|
|
1009
1035
|
scope, `prompt` for a preloaded memory) that goes to the call. A file opens
|
|
1010
1036
|
as its row's title does, a URL in a new tab. A row (on hover or keyboard
|
|
1011
1037
|
focus) and a folded group offer `drop`, which runs `/working-set drop` (see
|
|
1012
|
-
`/working-set` above); a dropped row offers `undrop
|
|
1038
|
+
`/working-set` above); a dropped row offers `undrop`, and so does one whose
|
|
1039
|
+
drop holds on some reads only (`part dropped` in `/working-set`'s listing:
|
|
1040
|
+
an older read stubbed by the stale layer, or a newer read sent). Between turns it
|
|
1013
1041
|
applies at once; while a turn runs it waits for the turn's end: the row is
|
|
1014
1042
|
dashed with `drops next turn` and an `undo`, and the list's foot counts the
|
|
1015
|
-
pending changes (`undo all`), in every tab and after a reload
|
|
1043
|
+
pending changes (`undo all`), in every tab and after a reload; the stage's
|
|
1044
|
+
rail dashes the item's line too (its tooltip says `drops next turn`). On a phone a
|
|
1016
1045
|
tap on a row opens its actions under it. `drop` is greyed, with why on
|
|
1017
1046
|
hover, for an item only edited, a memory only preloaded, or one already
|
|
1018
|
-
forgotten.
|
|
1047
|
+
forgotten. Under a strategy with forget a row also offers `pin`
|
|
1048
|
+
(`/working-set pin`, pending as `applies next turn` mid-turn); a pinned row
|
|
1049
|
+
shows `📌` and `unpin` (under any strategy), and its `drop` is greyed until
|
|
1050
|
+
you unpin it.
|
|
1051
|
+
The list's foot (a phone's last row) takes a path, `memory:NAME` or a URL
|
|
1052
|
+
and runs `/working-set add`: the item shows as a dashed `+ name` row with
|
|
1053
|
+
`reads next turn` and an `undo` until the next turn starts (the model is then
|
|
1054
|
+
asked to read it, and it joins the set as the model reads it). A stale or
|
|
1055
|
+
forgotten row offers `refresh`, the same add of it. While a turn runs the
|
|
1056
|
+
stage's rail shows an add queued for its end as a dashed `+ name · next turn`
|
|
1057
|
+
line.
|
|
1019
1058
|
|
|
1020
1059
|
Only `read`, `edit`, `write`, `memory_read`, `memory_write` and `web_fetch`
|
|
1021
1060
|
calls that succeeded count; files an `execute` touched don't.
|
|
@@ -1031,6 +1070,8 @@ the same live, after a reload and in every tab:
|
|
|
1031
1070
|
- **dropped** (struck): you dropped it (`/working-set drop`, the list's
|
|
1032
1071
|
`drop`); its reads are sent as stubs under every strategy until you undrop
|
|
1033
1072
|
it.
|
|
1073
|
+
- **pinned** (`📌`): you pinned it (`/working-set pin`, the list's `pin`);
|
|
1074
|
+
`forget_outputs` refuses it.
|
|
1034
1075
|
- **forgotten** (struck): nothing of the item's content is still sent to the
|
|
1035
1076
|
model under the session's current LLM context strategy (`forget_outputs`
|
|
1036
1077
|
forgot every read of it; see
|
|
@@ -1113,6 +1154,72 @@ access token", paste the token there (the part of the link after
|
|
|
1113
1154
|
- On http the browser has no notifications: the bell is hidden on the phone,
|
|
1114
1155
|
and the tab title still counts what needs you.
|
|
1115
1156
|
|
|
1157
|
+
### Remote sessions
|
|
1158
|
+
|
|
1159
|
+
The session commands also reach another machine's sessions, through its chi
|
|
1160
|
+
web on the LAN: the sessions, workers, memories and code stay there. On the
|
|
1161
|
+
other machine (the Studio), run `chi web --web-host lan`; on this one (the
|
|
1162
|
+
laptop), add it once with its token (the part of its LAN link after
|
|
1163
|
+
`token=`, or its `$XDG_STATE_HOME/samagotchi/web-token` file):
|
|
1164
|
+
|
|
1165
|
+
```sh
|
|
1166
|
+
chi remote add studio http://192.168.1.29:4567 --token-file studio-token
|
|
1167
|
+
# studio http://192.168.1.29:4567 chi 0.103.0, LAN 192.168.1.29
|
|
1168
|
+
chi --remote studio send --new --dir /Users/me/projects/app --wait -m "fix the flaky spec"
|
|
1169
|
+
chi --remote studio answer ab12 --question q1 --option 1
|
|
1170
|
+
CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
- `chi remote add NAME URL` checks the address and token first (`GET
|
|
1174
|
+
/api/info`) and says what it found: the chi it runs and its LAN address, or
|
|
1175
|
+
why not: nothing answers (start chi web there with `web.host: lan`), the
|
|
1176
|
+
token was refused, or a hostname (chi web answers its LAN IP only: use the
|
|
1177
|
+
IP, which changes with the Wi-Fi), or a chi too old for remote sessions.
|
|
1178
|
+
Nothing is saved then. The remotes live in
|
|
1179
|
+
`$XDG_CONFIG_HOME/samagotchi/remotes.yml` (0600), their tokens inside: a
|
|
1180
|
+
token runs commands as you there. `chi remote ls` (`--format json`) never
|
|
1181
|
+
prints one; `chi remote add` of an existing name replaces it.
|
|
1182
|
+
- `--remote NAME` goes before the command (`chi --remote studio note …`),
|
|
1183
|
+
`CHI_REMOTE=NAME` in the environment does the same for every command a
|
|
1184
|
+
script runs; the flag beats the variable, and there is no default remote.
|
|
1185
|
+
An unknown name exits 2.
|
|
1186
|
+
- What runs there: `send` (every form: `--new`, `--wait`, `--queue`,
|
|
1187
|
+
`--cut`, `--image`, a session command), `answer`, `note`, a session's
|
|
1188
|
+
`context add|push|ls|show|rm` and `sessions
|
|
1189
|
+
list|stop|restart|archive|unarchive|delete`. The rest runs on this machine
|
|
1190
|
+
only and exits 2 with `chi <command> runs on this machine only: run it on
|
|
1191
|
+
<remote>`: `context refresh|mute|unmute` and `--project`, `sessions
|
|
1192
|
+
stats|prune|clean`, `broadcast`, the machine's own commands (`bundle`,
|
|
1193
|
+
`memory`, `update`, …) and a session in this terminal (`--attach`, `-p`,
|
|
1194
|
+
`--resume`: remote attach comes later). `chi remote`, `--version` and
|
|
1195
|
+
`--help` always run here.
|
|
1196
|
+
- `send --new` needs `--dir`, that machine's absolute path (no folder of this
|
|
1197
|
+
one is guessed); `--toolkit`, `--set` and `--parent-answers` are checked
|
|
1198
|
+
against that machine's config and bundles. `sessions list` lists every
|
|
1199
|
+
project's sessions there (`--scope=project --cwd DIR` one project's) in the
|
|
1200
|
+
`--live` text form. `context` takes the session ids (no default session),
|
|
1201
|
+
and a URL resolves through that machine's bundles.
|
|
1202
|
+
- That machine's config.yml decides what it checks for `send` and `answer`:
|
|
1203
|
+
a parent agent's `/continue yes` and options (`guardrails.parent_answers`),
|
|
1204
|
+
a loosening `/parent-answers`, images to a model that can't see them. Each
|
|
1205
|
+
target refuses its own (`ab12 refused: …`), where this machine refuses
|
|
1206
|
+
before sending to any.
|
|
1207
|
+
- Waits run on the server, 30 s at a time, until the answer or your own
|
|
1208
|
+
`--timeout`; exit codes are the same. The commands chi prints for what to
|
|
1209
|
+
do next work as pasted: they carry `--remote NAME` (`chi --remote studio
|
|
1210
|
+
answer ab12 --question q1 --option N`, `chi --remote studio send --wait …`),
|
|
1211
|
+
under `CHI_REMOTE` too. What runs on this machine only isn't offered:
|
|
1212
|
+
where chi says `chi --attach ID` here it names that machine's chi web
|
|
1213
|
+
(`chi web on studio shows it`), and `sessions stop` names no `chi
|
|
1214
|
+
--resume`.
|
|
1215
|
+
- A remote that can't be reached names itself and why (`chi send: remote
|
|
1216
|
+
studio (http://192.168.1.29:4567): connection refused`, exit 1; with
|
|
1217
|
+
`--format json` an `error` object).
|
|
1218
|
+
- Plain http, the same as the phone's: use it on your home network.
|
|
1219
|
+
|
|
1220
|
+
How it works inside (`SessionAccess`, chi web's routes, where each check runs):
|
|
1221
|
+
[internals/remote-sessions.md](internals/remote-sessions.md).
|
|
1222
|
+
|
|
1116
1223
|
## Runtime Model Switch (Assist Mode)
|
|
1117
1224
|
|
|
1118
1225
|
In interactive assist mode, you can switch the request model without restarting:
|
data/docs/configuration.md
CHANGED
|
@@ -1485,7 +1485,13 @@ models:
|
|
|
1485
1485
|
forget keeps lines of it; a keep outside the output's lines, of a big file's head/tail preview, or holding all of
|
|
1486
1486
|
it; an output too small for a stub to free anything; one already stubbed or forgotten; an unknown id, or one whose
|
|
1487
1487
|
output chi can't pair with its call for sure (it shows no id). The system prompt, user messages, steers and context
|
|
1488
|
-
notes have no ids.
|
|
1488
|
+
notes have no ids. An output you dropped, or one of an item you pinned, is refused too (`dropped by the user`,
|
|
1489
|
+
`pinned by the user: it stays in your context`).
|
|
1490
|
+
- **Pins.** `/working-set pin KEY…` (or the web list's `pin`, shown under forget only) keeps an item from the model's
|
|
1491
|
+
forget: every read of it, those to come too, until `/working-set unpin KEY…` (see [cli.md](cli.md#the-working-set)).
|
|
1492
|
+
It sends nothing different, so the prompt cache holds. The pins are the session's own (`working_set_pins` in the
|
|
1493
|
+
session file): a respawned worker, a continue and a fork keep them, across a strategy switch too; a delegate child
|
|
1494
|
+
starts without. Stale stubs still apply to a pinned file's superseded reads (its newer read is sent).
|
|
1489
1495
|
- **When stubs reach the prompt** follows `llm_context.apply`, as `stale`'s do: the result says
|
|
1490
1496
|
`applied, the next request sends the stubs (frees …)` or `staged until the turn ends`.
|
|
1491
1497
|
- **Restore.** `forget_outputs(restore: ["t45"])` brings a forgotten output back with the next request, whatever
|
data/docs/guardrails.md
CHANGED
|
@@ -582,7 +582,8 @@ hooks, approvals, bundles or attached context sources, or a `.git/hooks`
|
|
|
582
582
|
(`shell-touches-chi`, `touches: chi_dirs`), unless they only read
|
|
583
583
|
(`skip_read_only`); `chi context add … --cmd` (`chi-context-cmd`, once at a
|
|
584
584
|
time: a command chi will run every few minutes outside the guardrails; see
|
|
585
|
-
[Attached context](context.md#safety)
|
|
585
|
+
[Attached context](context.md#safety); the rule also sees `chi --remote NAME context add`, and a remote's `context push`
|
|
586
|
+
reaches its session with the trust of its chi web token, as your own `chi context push` does there); `chi broadcast` (`chi-broadcast`,
|
|
586
587
|
once at a time and only by the user: it is the user's command, which also
|
|
587
588
|
refuses to run inside a session; see [Broadcast](broadcast.md)); removing a
|
|
588
589
|
memory with `memory_write remove: true` (`memory-remove`, once at a time:
|