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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -1
  3. data/bin/chi +60 -191
  4. data/docs/architecture.md +67 -1
  5. data/docs/cli.md +190 -9
  6. data/docs/configuration.md +7 -1
  7. data/docs/guardrails.md +4 -2
  8. data/docs/internals/context-telemetry.md +6 -2
  9. data/docs/internals/engine.md +5 -2
  10. data/docs/internals/llm-context-forget.md +11 -1
  11. data/docs/internals/participants.md +137 -0
  12. data/docs/internals/remote-sessions.md +429 -0
  13. data/docs/internals/working-set.md +95 -11
  14. data/docs/plugins.md +33 -3
  15. data/docs/sessions.md +16 -7
  16. data/docs/sub-agent.md +16 -0
  17. data/docs/testing.md +68 -4
  18. data/lib/samagotchi/answer_command.rb +27 -25
  19. data/lib/samagotchi/bridge.rb +31 -7
  20. data/lib/samagotchi/bridge_client/event_stream.rb +86 -12
  21. data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
  22. data/lib/samagotchi/bridge_client.rb +26 -39
  23. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
  25. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  26. data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
  27. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
  28. data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
  29. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  30. data/lib/samagotchi/chi_invocation.rb +40 -0
  31. data/lib/samagotchi/cli/command.rb +3 -2
  32. data/lib/samagotchi/cli/flags.rb +5 -0
  33. data/lib/samagotchi/cli/parent_wait.rb +22 -13
  34. data/lib/samagotchi/context_command.rb +80 -73
  35. data/lib/samagotchi/context_status.rb +15 -2
  36. data/lib/samagotchi/engine.rb +28 -14
  37. data/lib/samagotchi/guardrail_wiring.rb +7 -1
  38. data/lib/samagotchi/idle_recap.rb +8 -2
  39. data/lib/samagotchi/idle_scheduler.rb +34 -9
  40. data/lib/samagotchi/kernel_loop.rb +21 -2
  41. data/lib/samagotchi/launch_mode.rb +26 -0
  42. data/lib/samagotchi/llm/chat_loop.rb +8 -1
  43. data/lib/samagotchi/llm_context_forget.rb +13 -6
  44. data/lib/samagotchi/note_command.rb +8 -7
  45. data/lib/samagotchi/owner_lock.rb +65 -5
  46. data/lib/samagotchi/parent_report.rb +55 -43
  47. data/lib/samagotchi/participants.rb +122 -0
  48. data/lib/samagotchi/remote_attach_command.rb +119 -0
  49. data/lib/samagotchi/remote_command.rb +170 -0
  50. data/lib/samagotchi/remotes.rb +130 -0
  51. data/lib/samagotchi/run_options.rb +245 -0
  52. data/lib/samagotchi/send_command/busy.rb +58 -0
  53. data/lib/samagotchi/send_command/parent_gate.rb +23 -10
  54. data/lib/samagotchi/send_command/wait_target.rb +16 -3
  55. data/lib/samagotchi/send_command.rb +173 -108
  56. data/lib/samagotchi/session.rb +38 -9
  57. data/lib/samagotchi/session_access/contract.rb +167 -0
  58. data/lib/samagotchi/session_access/dispatch.rb +152 -0
  59. data/lib/samagotchi/session_access/http_transport.rb +109 -0
  60. data/lib/samagotchi/session_access/local.rb +400 -0
  61. data/lib/samagotchi/session_access/remote.rb +734 -0
  62. data/lib/samagotchi/session_access/web_bridge_client.rb +236 -0
  63. data/lib/samagotchi/session_access.rb +223 -0
  64. data/lib/samagotchi/session_archive_command.rb +9 -5
  65. data/lib/samagotchi/session_commands.rb +20 -15
  66. data/lib/samagotchi/session_continue.rb +1 -1
  67. data/lib/samagotchi/session_delete_command.rb +8 -4
  68. data/lib/samagotchi/session_inbox.rb +18 -9
  69. data/lib/samagotchi/session_manager.rb +27 -6
  70. data/lib/samagotchi/session_setup.rb +11 -6
  71. data/lib/samagotchi/sessions_command.rb +36 -14
  72. data/lib/samagotchi/share_command.rb +174 -0
  73. data/lib/samagotchi/terminal_ui/attach_launcher.rb +66 -17
  74. data/lib/samagotchi/terminal_ui/attach_link.rb +114 -0
  75. data/lib/samagotchi/terminal_ui/attached_loop.rb +155 -87
  76. data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
  77. data/lib/samagotchi/terminal_ui/local_link.rb +135 -0
  78. data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
  79. data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
  80. data/lib/samagotchi/terminal_ui/version_lines.rb +28 -3
  81. data/lib/samagotchi/terminal_ui.rb +6 -3
  82. data/lib/samagotchi/testing/waiting.rb +92 -9
  83. data/lib/samagotchi/title_command.rb +9 -11
  84. data/lib/samagotchi/tools/task_runtime.rb +17 -3
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +530 -126
  87. data/lib/samagotchi/web/grants.rb +302 -0
  88. data/lib/samagotchi/web/participant_access.rb +286 -0
  89. data/lib/samagotchi/web/principal.rb +29 -0
  90. data/lib/samagotchi/web/public/data.js +3 -2
  91. data/lib/samagotchi/web/public/index.html +20 -4
  92. data/lib/samagotchi/web/public/stage_frame.js +7 -2
  93. data/lib/samagotchi/web/public/stage_view.js +3 -2
  94. data/lib/samagotchi/web/public/stream_handlers.js +3 -1
  95. data/lib/samagotchi/web/public/working_set.js +159 -28
  96. data/lib/samagotchi/web/public/working_set_rail.js +39 -12
  97. data/lib/samagotchi/web/public/working_set_view.js +91 -33
  98. data/lib/samagotchi/web/server.rb +3 -1
  99. data/lib/samagotchi/web/session_access_routes.rb +459 -0
  100. data/lib/samagotchi/worker.rb +26 -5
  101. data/lib/samagotchi/worker_sidecar.rb +12 -5
  102. data/lib/samagotchi/working_set.rb +88 -14
  103. data/lib/samagotchi/working_set_command.rb +133 -30
  104. data/lib/samagotchi/working_set_pin.rb +34 -0
  105. data/lib/samagotchi/working_set_steer.rb +191 -27
  106. metadata +28 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '084d47c40205bd43fa1126dd3169a0792cc6faee949899bf6b62d84a559d1501'
4
- data.tar.gz: 23dde4e086c819fc43349cb20afed56121fb51bfb3b54b9be6e90fd8a332003b
3
+ metadata.gz: f4a794e9e201d233ca72705b62746997d6ef702804e18ad8a5bb1f85851ed454
4
+ data.tar.gz: 341e3b0cfcbc4367faed3782d600b2d934395eef7623499619f018b1984a06db
5
5
  SHA512:
6
- metadata.gz: 763b8db4ab722ec994871d8d5e4e1238beda7ae846c9cf8bf5f9dc0f2c8d545eb767b7d82afcf35a8cb096f2f30461a8a793068c5359d5c926a7209e626c91b9
7
- data.tar.gz: abb0665762926a27e66133b5326cf8b18658e5404047d9f1ce414c6455ac3ddffa15667c00ab6a6ad929a9d15d451f667ad11b702d08bccb9fa9053161415a5b
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.102.0...HEAD
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
- # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
80
- options = { verbose: false, no_interrupt: Samagotchi::Config.get("no_interrupt"), non_interactive: false, no_default_input: Samagotchi::Config.get("no_default_input"), web: false, web_port: nil, web_open: false, web_markdown: nil }
81
- cli_overrides = {}
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
- warn "Error: that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
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
- conflict = { non_interactive: "--non-interactive", verbose: "--verbose" }
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`). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
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)). |