samagotchi 0.103.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -1
- data/bin/chi +39 -2
- data/docs/architecture.md +36 -24
- data/docs/cli.md +86 -12
- data/docs/guardrails.md +2 -1
- data/docs/internals/context-telemetry.md +6 -2
- data/docs/internals/engine.md +4 -1
- data/docs/internals/participants.md +137 -0
- data/docs/internals/remote-sessions.md +153 -27
- data/docs/plugins.md +7 -1
- data/docs/sessions.md +4 -2
- data/docs/sub-agent.md +8 -0
- data/docs/testing.md +68 -4
- data/lib/samagotchi/bridge.rb +7 -3
- data/lib/samagotchi/bridge_client/event_stream.rb +9 -4
- data/lib/samagotchi/bridge_client.rb +4 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +13 -16
- data/lib/samagotchi/context_status.rb +15 -2
- data/lib/samagotchi/engine.rb +16 -6
- data/lib/samagotchi/guardrail_wiring.rb +7 -1
- data/lib/samagotchi/idle_recap.rb +1 -1
- data/lib/samagotchi/idle_scheduler.rb +34 -9
- data/lib/samagotchi/kernel_loop.rb +8 -1
- data/lib/samagotchi/launch_mode.rb +4 -0
- data/lib/samagotchi/llm/chat_loop.rb +8 -1
- data/lib/samagotchi/owner_lock.rb +65 -5
- data/lib/samagotchi/participants.rb +122 -0
- data/lib/samagotchi/remote_attach_command.rb +119 -0
- data/lib/samagotchi/run_options.rb +14 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session.rb +8 -4
- data/lib/samagotchi/session_access/contract.rb +42 -4
- data/lib/samagotchi/session_access/dispatch.rb +29 -10
- data/lib/samagotchi/session_access/local.rb +52 -1
- data/lib/samagotchi/session_access/remote.rb +112 -16
- data/lib/samagotchi/session_access/web_bridge_client.rb +56 -1
- data/lib/samagotchi/session_access.rb +85 -3
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_inbox.rb +18 -9
- data/lib/samagotchi/session_manager.rb +21 -5
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +174 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +17 -4
- data/lib/samagotchi/terminal_ui/attach_link.rb +10 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +83 -20
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +5 -0
- data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
- data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +23 -0
- data/lib/samagotchi/terminal_ui.rb +4 -1
- data/lib/samagotchi/testing/waiting.rb +92 -9
- data/lib/samagotchi/tools/task_runtime.rb +17 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +187 -74
- data/lib/samagotchi/web/grants.rb +302 -0
- data/lib/samagotchi/web/participant_access.rb +286 -0
- data/lib/samagotchi/web/principal.rb +29 -0
- data/lib/samagotchi/web/server.rb +3 -1
- data/lib/samagotchi/web/session_access_routes.rb +120 -4
- data/lib/samagotchi/worker.rb +26 -5
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4a794e9e201d233ca72705b62746997d6ef702804e18ad8a5bb1f85851ed454
|
|
4
|
+
data.tar.gz: 341e3b0cfcbc4367faed3782d600b2d934395eef7623499619f018b1984a06db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20bdcd9c915786fe5698401079a36e676ace823d4e42625c5b75fd2472d9990b342f1cc9c23e2a609bffee3b57df88c4fb9b6c927cdf0e182c1f7966fc5fde37
|
|
7
|
+
data.tar.gz: 64d05e8f627bb8c628e3510681b7acf1de4c37995175333614094d00333f66b1de28d91a6b2819f005e4d1e5150c8ffd189f6c9e2379044b89268188086231e6
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,58 @@ 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
|
+
|
|
11
63
|
## [0.103.0] - 2026-10-23
|
|
12
64
|
|
|
13
65
|
### Added
|
|
@@ -3892,7 +3944,8 @@ and long-lived sessions.
|
|
|
3892
3944
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3893
3945
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3894
3946
|
|
|
3895
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3947
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.104.0...HEAD
|
|
3948
|
+
[0.104.0]: https://github.com/dm1try/samagotchi/compare/v0.103.0...v0.104.0
|
|
3896
3949
|
[0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
|
|
3897
3950
|
[0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
|
|
3898
3951
|
[0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
|
data/bin/chi
CHANGED
|
@@ -52,6 +52,8 @@ SUBCOMMANDS = {
|
|
|
52
52
|
"send" => ["samagotchi/send_command", "SendCommand"],
|
|
53
53
|
# answer the question a session waits on, then wait (--question, --option)
|
|
54
54
|
"answer" => ["samagotchi/answer_command", "AnswerCommand"],
|
|
55
|
+
# invite participants to review a session, list them, revoke them
|
|
56
|
+
"share" => ["samagotchi/share_command", "ShareCommand"],
|
|
55
57
|
# find a model server and write config.yml (--model, --name)
|
|
56
58
|
"bootstrap" => ["samagotchi/bootstrap_command", "BootstrapCommand"],
|
|
57
59
|
# bring an installed chi up to date
|
|
@@ -69,7 +71,10 @@ SUBCOMMANDS = {
|
|
|
69
71
|
}.freeze
|
|
70
72
|
# chi --remote NAME <command> (or CHI_REMOTE=NAME): the session commands
|
|
71
73
|
# against another machine's chi web; what runs here only is refused (exit 2).
|
|
72
|
-
# chi remote, --version and --help run here as always.
|
|
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.
|
|
73
78
|
require "samagotchi/session_access/dispatch"
|
|
74
79
|
begin
|
|
75
80
|
remote_run = Samagotchi::SessionAccess::Dispatch.from(ARGV)
|
|
@@ -78,7 +83,12 @@ rescue Samagotchi::SessionAccess::Dispatch::Usage => e
|
|
|
78
83
|
exit 2
|
|
79
84
|
end
|
|
80
85
|
ARGV.replace(remote_run.argv)
|
|
81
|
-
|
|
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
|
|
82
92
|
|
|
83
93
|
if (path, klass = SUBCOMMANDS[ARGV[0]])
|
|
84
94
|
require path
|
|
@@ -152,6 +162,29 @@ Samagotchi::Config.reload!(cli_overrides: cli_overrides) unless cli_overrides.em
|
|
|
152
162
|
require "samagotchi/log"
|
|
153
163
|
Samagotchi::Log.configure(level: options[:verbose] ? :debug : nil, mirror: options[:verbose])
|
|
154
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
|
+
|
|
155
188
|
# `chi web`: its flags (--port, --open, --web-view…) went through the parser above.
|
|
156
189
|
options[:web] = true if web
|
|
157
190
|
|
|
@@ -340,6 +373,10 @@ if launch == :attached
|
|
|
340
373
|
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
341
374
|
warn "Error: #{e.message}"
|
|
342
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
|
|
343
380
|
end
|
|
344
381
|
exit Samagotchi::TerminalUI::AttachLauncher.exit_status(ended)
|
|
345
382
|
end
|
data/docs/architecture.md
CHANGED
|
@@ -469,7 +469,8 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
|
|
|
469
469
|
The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
|
|
470
470
|
for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
|
|
471
471
|
(503 `not_live` when the worker is not running; full history of any session is served by
|
|
472
|
-
`GET /api/sessions/:id/output`)
|
|
472
|
+
`GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
|
|
473
|
+
request principal's frame filter (`App#stream_filter`, nil for the owner, the only principal today). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
|
|
473
474
|
an earlier worker gets the `reset` snapshot, not a replay.
|
|
474
475
|
|
|
475
476
|
**Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
|
|
@@ -510,35 +511,46 @@ chi web (the remote-sessions plan; `SessionAccess::Contract` says which commands
|
|
|
510
511
|
cover most of it; `Web::SessionAccessRoutes` (`web/session_access_routes.rb`, in `App`'s `ROUTES`) adds the rest, each
|
|
511
512
|
calling the `SessionAccess::Local` operation of the same name, so a remote backend maps 1:1 onto them. Every one is
|
|
512
513
|
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`).
|
|
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
|
|
514
519
|
model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
515
520
|
|
|
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
|
|
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/
|
|
536
|
-
| `POST /api/sessions/:id/
|
|
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 |
|
|
537
548
|
|
|
538
549
|
`GET /api/info` names the feature `session_commands` (`SessionAccessRoutes::FEATURE`): `SessionAccess::Remote` asks it
|
|
539
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
|
|
540
551
|
old for this command through --remote: update chi there (chi update)". The feature `remote_attach`
|
|
541
|
-
(`SessionAccessRoutes::ATTACH_FEATURE`) names the
|
|
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}`.
|
|
542
554
|
|
|
543
555
|
`POST …/answer` passes on `client_id` only when it is chi answer's (`cli:answer`), so the worker holds the answer to
|
|
544
556
|
its `guardrails.parent_answers`; any other is dropped, so a forwarded id can only tighten. The worker's 403
|
data/docs/cli.md
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
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
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 --
|
|
25
|
+
- `chi share ID --name NAME [--agent] [--expires 3d]`, `chi share ls [ID]`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days), list them, revoke them (see [Participants](#participants))
|
|
26
|
+
- `chi --remote NAME send|answer|note|context|sessions|share …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
|
|
27
|
+
- `chi --remote NAME --attach ID` (`--resume ID`, `--dir DIR [-p TEXT]`) — the attached TUI on a session of that machine (see [Remote sessions](#remote-sessions))
|
|
26
28
|
- `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))
|
|
27
29
|
- `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))
|
|
28
30
|
- `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))
|
|
@@ -280,7 +282,8 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
|
|
|
280
282
|
| `--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). |
|
|
281
283
|
| `--no-shared` | Run the plain in-process REPL for this run. |
|
|
282
284
|
| `--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). |
|
|
285
|
+
| `--remote NAME` | Before a session command (`chi --remote studio send …`) or the run flags (`chi --remote studio --attach ID`, `--resume ID`, `--dir DIR -p TEXT`): 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). |
|
|
286
|
+
| `--dir DIR` | With `--remote`: a new session's folder on that machine, its absolute path (required for a new session there). Without `--remote` it exits 2: cd there instead. |
|
|
284
287
|
| `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
|
|
285
288
|
| `--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. |
|
|
286
289
|
| `--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). |
|
|
@@ -750,12 +753,14 @@ REPL alike:
|
|
|
750
753
|
with images works the same way: it waits for the turn's end, and the lines
|
|
751
754
|
sent after it still go in at the next step.
|
|
752
755
|
At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
|
|
753
|
-
as a normal prompt
|
|
756
|
+
as a normal prompt, and the terminals say so: `(nothing is running: sent now)`.
|
|
754
757
|
- `/unqueue` (attached terminal) takes back the newest line this terminal
|
|
755
758
|
queued that hasn't started: `removed: <its first 60 characters>`, and its text
|
|
756
759
|
goes back into the input to change and send again (not over typed text: then
|
|
757
760
|
it is in the input history, ↑; not with input from a pipe; its images don't
|
|
758
|
-
come back), then the one before it on the next `/unqueue
|
|
761
|
+
come back), then the one before it on the next `/unqueue`. A command typed
|
|
762
|
+
after that text without clearing it (`fix it/unqueue`, `fix it/queue more`)
|
|
763
|
+
runs as the command, and the text stays in the input. `(nothing queued: …)` when none waits,
|
|
759
764
|
`(not removed: it already started)` when its turn won the race, `(not removed:
|
|
760
765
|
it is already gone)` when another client removed it. Other terminals that
|
|
761
766
|
showed it say `(a queued message was removed)`; the web drops its bubble. A
|
|
@@ -1189,10 +1194,9 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
|
|
|
1189
1194
|
list|stop|restart|archive|unarchive|delete`. The rest runs on this machine
|
|
1190
1195
|
only and exits 2 with `chi <command> runs on this machine only: run it on
|
|
1191
1196
|
<remote>`: `context refresh|mute|unmute` and `--project`, `sessions
|
|
1192
|
-
stats|prune|clean`, `broadcast
|
|
1193
|
-
`memory`, `update`, …)
|
|
1194
|
-
|
|
1195
|
-
`--help` always run here.
|
|
1197
|
+
stats|prune|clean`, `broadcast` and the machine's own commands (`bundle`,
|
|
1198
|
+
`memory`, `update`, …). `chi remote`, `--version` and `--help` always run
|
|
1199
|
+
here.
|
|
1196
1200
|
- `send --new` needs `--dir`, that machine's absolute path (no folder of this
|
|
1197
1201
|
one is guessed); `--toolkit`, `--set` and `--parent-answers` are checked
|
|
1198
1202
|
against that machine's config and bundles. `sessions list` lists every
|
|
@@ -1208,18 +1212,88 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
|
|
|
1208
1212
|
`--timeout`; exit codes are the same. The commands chi prints for what to
|
|
1209
1213
|
do next work as pasted: they carry `--remote NAME` (`chi --remote studio
|
|
1210
1214
|
answer ab12 --question q1 --option N`, `chi --remote studio send --wait …`),
|
|
1211
|
-
under `CHI_REMOTE` too
|
|
1212
|
-
|
|
1213
|
-
(`chi web on studio shows it`), and `sessions stop` names no `chi
|
|
1214
|
-
--resume`.
|
|
1215
|
+
under `CHI_REMOTE` too, an attach as well (`chi --remote studio --attach
|
|
1216
|
+
ab12 shows it`, `sessions stop`'s `chi --remote studio --resume ab12`).
|
|
1215
1217
|
- A remote that can't be reached names itself and why (`chi send: remote
|
|
1216
1218
|
studio (http://192.168.1.29:4567): connection refused`, exit 1; with
|
|
1217
1219
|
`--format json` an `error` object).
|
|
1218
1220
|
- Plain http, the same as the phone's: use it on your home network.
|
|
1219
1221
|
|
|
1222
|
+
A session there in this terminal (the attached TUI, through that machine's
|
|
1223
|
+
chi web; its worker stays there):
|
|
1224
|
+
|
|
1225
|
+
```sh
|
|
1226
|
+
chi --remote studio --attach ab12 # attach (wakes its worker if it exited)
|
|
1227
|
+
chi --remote studio --resume ab12 -p "go on" # resume there, run one turn, stay attached
|
|
1228
|
+
chi --remote studio --dir /Users/me/app -p "hi" # a new session there (--dir required), one turn, stay
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
- The same TUI and exit codes (0 detached, 3 a question left waiting with
|
|
1232
|
+
input from a pipe, 1 otherwise), every printed command with `--remote
|
|
1233
|
+
studio`. Prompts, session commands, `/model`, `!cmd` (a shell command **on
|
|
1234
|
+
that machine**, under its guardrails), `/queue`, `/cut`, `/unqueue`,
|
|
1235
|
+
`/detach`, `/exit`, `/exit --delete`, `/archive`, `/stats` and `/recap` work
|
|
1236
|
+
as here, on that machine's session (`/exit --delete` deletes it there).
|
|
1237
|
+
The status row's children segment counts its delegates there, and a join
|
|
1238
|
+
shows the steps' durations and a failed turn's line from its saved records
|
|
1239
|
+
there. A chi web too old for one of them says so in one line ("remote
|
|
1240
|
+
studio runs chi X, too old for this command through --remote: update chi
|
|
1241
|
+
there").
|
|
1242
|
+
- A new session needs `--dir`, that machine's absolute path; `--model`,
|
|
1243
|
+
`--thinking`, `--llm-context*`, `--toolkit`, `--tool`, `--no-tool`, `--set`
|
|
1244
|
+
and `--parent-answers` are checked there (exit 2 for a bad one). With
|
|
1245
|
+
`--attach`/`--resume` they work as here (`--model`, `--thinking`,
|
|
1246
|
+
`--llm-context*` change the session; the others are named as ignored).
|
|
1247
|
+
`--non-interactive` (use `chi --remote studio send --new --dir D -m TEXT
|
|
1248
|
+
--wait`), `--verbose`, `--no-shared`, `--memory`, `--mute` and
|
|
1249
|
+
`--append-system-prompt` run on this machine only (exit 2).
|
|
1250
|
+
- `@path` images are this machine's files (read and downscaled here, then
|
|
1251
|
+
uploaded); Tab completes `/` commands and `@path`, not memory names. The
|
|
1252
|
+
prompt history (↑) and `no_interrupt` / `no_default_input` are this
|
|
1253
|
+
terminal's.
|
|
1254
|
+
- When chi web can't be reached the TUI says "(lost the connection to
|
|
1255
|
+
studio; reconnecting…)" and tries for about a minute, then "(reconnected)";
|
|
1256
|
+
what it missed is replayed or resynced. A prompt typed meanwhile is usually
|
|
1257
|
+
accepted there and runs when a worker reads it. A worker that went away
|
|
1258
|
+
(not stopped) is woken again. The end is worded by why: "The session was
|
|
1259
|
+
stopped on studio. Resume it with: chi --remote studio --resume ab12", the
|
|
1260
|
+
token refused, chi web unreachable, the worker gone, the session deleted.
|
|
1261
|
+
- The version line names both machines: the worker older than what studio
|
|
1262
|
+
has installed (`chi --remote studio sessions restart ab12`), this terminal
|
|
1263
|
+
older than studio (`chi update` here).
|
|
1264
|
+
|
|
1220
1265
|
How it works inside (`SessionAccess`, chi web's routes, where each check runs):
|
|
1221
1266
|
[internals/remote-sessions.md](internals/remote-sessions.md).
|
|
1222
1267
|
|
|
1268
|
+
### Participants
|
|
1269
|
+
|
|
1270
|
+
A session's owner can invite **participants**, people or agents, to review it through chi web on the LAN
|
|
1271
|
+
(`web.host: lan`), each with a link of their own that reaches that one session:
|
|
1272
|
+
|
|
1273
|
+
```sh
|
|
1274
|
+
chi share 3f9a1c2e --name Alex
|
|
1275
|
+
# p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02
|
|
1276
|
+
# http://192.168.1.29:4567/?token=chp_…#s=3f9a1c2e-…
|
|
1277
|
+
# (shown once: chi keeps only its digest; chi share revoke 3f9a1c2e p_7c01d2aa ends it)
|
|
1278
|
+
chi share ls # every session's participants (--format json; never a token)
|
|
1279
|
+
chi share revoke 3f9a1c2e # every link to that session; add a participant id for one
|
|
1280
|
+
chi share revoke --all # every link to every session
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
- The link is printed once (with a QR code at a terminal): chi keeps only a digest of its token. Send it to the
|
|
1284
|
+
participant; opening it sets their browser's cookie, as the owner's LAN link does. `--agent` marks an agent
|
|
1285
|
+
participant, which uses the token as `Authorization: Bearer` (e.g. `chi --remote` with that token).
|
|
1286
|
+
- It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days), until it is revoked, or until the session is
|
|
1287
|
+
archived or deleted. A revoked or expired link shows "This review link has expired or was revoked."
|
|
1288
|
+
- For now a participant sees a page with the session's title and status; comments and the review itself come in
|
|
1289
|
+
a later chi. They can't reach anything else: every other route answers them 403, another session 404.
|
|
1290
|
+
- `chi --remote studio share …` invites on studio: the link names studio's LAN address. Locally, the link names
|
|
1291
|
+
this machine's chi web; without one on the LAN, chi says the link opens on this machine only.
|
|
1292
|
+
- The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
|
|
1293
|
+
|
|
1294
|
+
How it works inside (grants, the gate, the routes, the audit trail):
|
|
1295
|
+
[internals/participants.md](internals/participants.md).
|
|
1296
|
+
|
|
1223
1297
|
## Runtime Model Switch (Assist Mode)
|
|
1224
1298
|
|
|
1225
1299
|
In interactive assist mode, you can switch the request model without restarting:
|
data/docs/guardrails.md
CHANGED
|
@@ -595,7 +595,8 @@ only by the user: these change a model's system prompt for later sessions (see
|
|
|
595
595
|
[Model notes](memory.md#model-notes); a shell command reaches them only under
|
|
596
596
|
`shell-touches-chi`'s text heuristic); and answering chi's
|
|
597
597
|
questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
|
|
598
|
-
pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped
|
|
598
|
+
pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped`, which also sees
|
|
599
|
+
`chi --remote NAME --attach|--resume|-p` and `CHI_REMOTE=NAME chi …`), and `curl`/`wget` to a
|
|
599
600
|
session's `/answer` route (`chi-answer-http`), both once or for the session, and only by the
|
|
600
601
|
user (a parent agent allowing one would answer as the user).
|
|
601
602
|
`chi answer` itself isn't asked about. It denies writes into `.git/hooks`.
|
|
@@ -12,8 +12,12 @@ resumed session's line already names):
|
|
|
12
12
|
|
|
13
13
|
Both loops do this, before each request (`ContextStatus`, one per turn). The
|
|
14
14
|
chat loop's estimate counts the conversation's text and tool calls, not the
|
|
15
|
-
tool schemas it sends, until the host's first count (its `usage`).
|
|
16
|
-
|
|
15
|
+
tool schemas it sends, until the host's first count (its `usage`). So that a
|
|
16
|
+
turn's first request doesn't dip below the last turn's count (the schemas are
|
|
17
|
+
thousands of tokens), the loop keeps the last counted request's chars per token
|
|
18
|
+
(`ContextStatus#counted_chars_per_token`, on `KernelLoop` / `ChatLoop`) and the
|
|
19
|
+
next turn's estimate before its first count uses it instead of
|
|
20
|
+
`context.chars_per_token`. The event carries a `status` string using this prefix:
|
|
17
21
|
|
|
18
22
|
`CONTEXT_STATUS ...`
|
|
19
23
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -518,7 +518,10 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
518
518
|
| `:answer_display` | after the after_turn hooks (observers only, not the sink) | `{display, refs?}` (display a string, or nil; refs the count of the answer's refs, only when a hook attached some) |
|
|
519
519
|
|
|
520
520
|
The boundary events (`:turn_started`, `:turn_completed`, `:turn_canceled`, `:turn_failed`) also carry `origin`
|
|
521
|
-
when the turn was queued with one
|
|
521
|
+
when the turn was queued with one: `{client_id:, enqueued_id:}`, plus `author:` when someone other than the session's
|
|
522
|
+
owner wrote it (its input file's `author`, stamped by chi web from the request's `Web::Principal`). An owner's turn
|
|
523
|
+
has no `author` anywhere: not in its input file, its origin or the session. `GuardrailWiring#begin_turn` gets the
|
|
524
|
+
origin, and `#turn_author` reads its author for the gate's checks.
|
|
522
525
|
|
|
523
526
|
### Hook points (not events)
|
|
524
527
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Participants: identity and routes
|
|
2
|
+
|
|
3
|
+
A session's owner can invite **participants** to review it: people, or agents, who reach that one session through
|
|
4
|
+
chi web with a link of their own. This page is the identity layer (the shared-sessions plan's P2): grants, the gate,
|
|
5
|
+
the routes a participant reaches, and what is written where. A participant can't send a turn yet (P3), and their page
|
|
6
|
+
shows only the session's title and status until the full participant UI lands (P4). The code and docs say
|
|
7
|
+
"participants" and "review", never "shared": `session.shared` already means "attached by default".
|
|
8
|
+
|
|
9
|
+
Until `web.public_url` lands, participants are a **LAN** feature (`web.host: lan`): a loopback peer is always the
|
|
10
|
+
owner, and a reverse proxy on the same machine would make every visitor loopback.
|
|
11
|
+
|
|
12
|
+
## Grants
|
|
13
|
+
|
|
14
|
+
A grant (`Web::Grants`, `lib/samagotchi/web/grants.rb`) binds **one random token** to **one session**, **one
|
|
15
|
+
participant** and **one role**, until it expires or is revoked.
|
|
16
|
+
|
|
17
|
+
- **Token.** `chp_` + 43 URL-safe characters (32 random bytes, as the owner's `Web::Token`). The prefix lets the gate
|
|
18
|
+
look up only participant tokens, and a leaked one is recognisable. It is shown **once**, in the link chi share or
|
|
19
|
+
`POST …/participants` prints; chi keeps only its **SHA-256 digest**. Lookup digests the presented token and
|
|
20
|
+
compares it with every grant's digest (`Rack::Utils.secure_compare`).
|
|
21
|
+
- **Participant.** The grant's `id`, `p_<8 hex>`, names the participant: a turn they send will be authored
|
|
22
|
+
`"p:<id>"` (`Grant#author`, `Principal#author`; P3). `kind` is `human` or `agent` (D11: an agent uses the same
|
|
23
|
+
token, as a Bearer header; there is no agent UI). `role` is `reviewer`, the only one in v1. `name` is cleaned to
|
|
24
|
+
one line of at most 64 characters with no brackets (`Grants.clean_name`), so it can't forge P3's
|
|
25
|
+
`[Review contribution from …]` header.
|
|
26
|
+
- **Lifetime (D8).** 7 days by default (`chi share --expires Nh|Nd`, 1 hour to 30 days). Revoked by the owner
|
|
27
|
+
(`chi share revoke`, `DELETE …/participants`), and by an **archive** (the session and its delegates) or a
|
|
28
|
+
**delete** (`SessionManager.archive_session` / `delete_session` → `Participants.end_sessions`). A session pruned by
|
|
29
|
+
retention, or archived or deleted by a process that didn't revoke, is caught by the gate: a live grant whose session
|
|
30
|
+
file is gone or archived is revoked on sight (`revoked_by: gone | archive`). Unarchiving doesn't restore a grant.
|
|
31
|
+
An ended grant stays in the store for 30 days, so its token answers `token_revoked` / `token_expired` rather than a
|
|
32
|
+
bare 401; then it goes.
|
|
33
|
+
- **Store.** `$XDG_STATE_HOME/samagotchi/web-grants/grants.json`, next to `web-token`: the file 0600, in its own 0700
|
|
34
|
+
folder, written whole (`AtomicFile`) under a lock file (`grants.lock`) since chi web and a local `chi share` both
|
|
35
|
+
write it. Readers follow its mtime (a `chi share revoke` in another process takes effect at chi web's next request).
|
|
36
|
+
`last_used_at` is written at most once a minute per grant. **chi web refuses to start** when that folder or file can
|
|
37
|
+
be read by another user, or isn't this user's (`Grants.check_permissions!`, one message naming the `chmod`). Only
|
|
38
|
+
those two are checked: the state dir above is often 0755, and that's fine. Never point `XDG_STATE_HOME` at a
|
|
39
|
+
shared folder.
|
|
40
|
+
- Rotating the owner token (`chi web --new-token`) never touches grants; `chi share revoke --all` ends them all.
|
|
41
|
+
|
|
42
|
+
## The gate
|
|
43
|
+
|
|
44
|
+
`App#principal_for` resolves every request to a `Web::Principal`:
|
|
45
|
+
|
|
46
|
+
1. A loopback peer, or a chi web not on the LAN: the **owner** (unchanged).
|
|
47
|
+
2. The owner's token (Bearer, then the cookie; `?token=` on the page's trade): the **owner**. Checked first, so the
|
|
48
|
+
owner's requests never touch the grants store (measured: no change to an owner route's time).
|
|
49
|
+
3. A `chp_` token whose grant is active: a **participant** (`Principal.new(kind: :participant, grant:)`).
|
|
50
|
+
4. Anything else: nil. `#token_gate` answers an ended grant's token **401 `token_revoked`** or **401
|
|
51
|
+
`token_expired`** (the page: "This review link has expired or was revoked. Ask the session's owner for a new
|
|
52
|
+
one."), any other **401 `unauthorized`** as before.
|
|
53
|
+
|
|
54
|
+
The `?token=` trade sets the `chi_token` cookie to the token that was presented, the owner's or the participant's
|
|
55
|
+
(one at a time per browser: a participant link opened in the owner's browser replaces the owner's cookie there).
|
|
56
|
+
|
|
57
|
+
Then `#route`, for each matching `ROUTES` row:
|
|
58
|
+
|
|
59
|
+
1. A participant on a `/api/sessions/:id…` route whose id isn't their grant's session gets **404 `not_found`**, the
|
|
60
|
+
same answer as a session that doesn't exist, before the permission check: they can't learn which sessions exist.
|
|
61
|
+
2. A route whose permission the principal lacks (`Principal#permits?`) gets **403 `not_permitted`**, worded for the
|
|
62
|
+
role: "a participant can't do that: only the session's owner can".
|
|
63
|
+
|
|
64
|
+
`spec/web/participant_gate_spec.rb` walks `ROUTES`: every `:owner` row answers a participant 403, and every session
|
|
65
|
+
row answers another session's id 404, so a new route is covered without a new example.
|
|
66
|
+
|
|
67
|
+
## What a participant reaches (P2)
|
|
68
|
+
|
|
69
|
+
| Route | Permission | A participant gets |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `GET /` | participant | the participant page (server-rendered, no script): "Alex's review", the session's title and status, "Comments open soon", the participants' names. Never the owner's page, where every route would refuse them. |
|
|
72
|
+
| `GET /api/info` | participant | `{app, version, features}` (what `SessionAccess::Remote`'s check needs); not `pid`, `cwd`, `lan`, `installed` |
|
|
73
|
+
| `GET /api/sessions/:id` | participant | their session only, whatever the query (`brief`, `cards`, `tail`, `timing`): `{session: {id, title, status, participants: [{id, name, kind, role}]}}` (the active ones). `title` is the session's own title, or "an untitled session": never `display_title`'s fallback, the first prompt's opening words |
|
|
74
|
+
|
|
75
|
+
Everything else is `:owner`, `POST …/turn` included (P3 opens it). Static assets (`/app.js`, …) are served to any
|
|
76
|
+
principal past the gate: they are the public frontend.
|
|
77
|
+
|
|
78
|
+
**Streams.** No stream route is a participant's yet (`App#stream_filter` raises for one, until P4's filter). The
|
|
79
|
+
seam for ending them is in place: `App#stream_running(principal)` is the proxy's running check (asked every
|
|
80
|
+
`BridgeClient::STREAM_POLL`), and for a participant it is false once the grant is revoked or expired, so P4's
|
|
81
|
+
participant streams close within a poll of a revoke, including one made by `chi share revoke` in another process.
|
|
82
|
+
|
|
83
|
+
## The owner's routes
|
|
84
|
+
|
|
85
|
+
| Route | Body / params | Answer | Refusals |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `GET /api/sessions/:id/participants` | | 200 `{participants: [grant]}`, every state | |
|
|
88
|
+
| `GET /api/participants` | | 200 `{participants: [grant]}`, every session's | |
|
|
89
|
+
| `POST /api/sessions/:id/participants` | `name`, `kind` (`human`/`agent`), `ttl` (seconds) | 201 `{participant: grant, token, link, lan}`: the token and the link once; `link` names this chi web's LAN address (`http://IP:PORT/?token=…#s=<id>`), `lan` false when it has none | 400 `invalid_participant` (name, kind, ttl, an archived session); 404 |
|
|
90
|
+
| `DELETE /api/sessions/:id/participants[?id=p_…]` | | 200 `{revoked: [grant]}`: one participant, or the session's | 400 `invalid_participant` |
|
|
91
|
+
| `DELETE /api/participants` | | 200 `{revoked: [grant]}`: every session's | |
|
|
92
|
+
|
|
93
|
+
A grant here is `Grant#public_h`: `id, session_id, kind, name, role, created_at, expires_at, revoked_at, revoked_by,
|
|
94
|
+
last_used_at, state` (`active | revoked | expired`). Never the token or its digest.
|
|
95
|
+
|
|
96
|
+
`chi share` (docs/cli.md) uses them through `--remote`, and `SessionAccess::Local`'s same-named operations here
|
|
97
|
+
(`participants`, `invite`, `revoke_participants`; `ParticipantView`, `Invitation`). A local invite's link names
|
|
98
|
+
the local chi web's LAN address when one runs (its `GET /api/info`), else `127.0.0.1` with a line saying it opens on
|
|
99
|
+
this machine only.
|
|
100
|
+
|
|
101
|
+
## Files in the session dir
|
|
102
|
+
|
|
103
|
+
Neither is in the session JSON (chi web never saves that, and a worker would overwrite a field it added), and
|
|
104
|
+
neither is copied to a continue, a fork or a delegate (a continue starts with no participants). A session nobody was
|
|
105
|
+
invited to has neither: its files stay byte for byte as they were.
|
|
106
|
+
|
|
107
|
+
- **`participants.json`** (0600, `Participants.write`): `{participants: [{id, kind, name, role, added_at,
|
|
108
|
+
expires_at, removed_at}]}`, rewritten by whoever changes a grant (chi web, `chi share`). Never a token or a digest.
|
|
109
|
+
The worker reads it with `Participants.read` (P3a's system-prompt line, `ctx`).
|
|
110
|
+
- **`participants.jsonl`** (0600, append-only, `Participants.log`): one JSON line per event, with `at`: `issued`
|
|
111
|
+
(participant, name, kind, role, expires_at), `revoked` (participant, by), `expired` (the first refused use after the
|
|
112
|
+
expiry, once per chi web), `peer` (the first use and each new peer address since chi web started: ip, user agent),
|
|
113
|
+
`refused` (method, path, reason; at most once a minute per route). It goes with the session (retention, delete).
|
|
114
|
+
|
|
115
|
+
## Errors through `--remote`
|
|
116
|
+
|
|
117
|
+
Each condition has one message, owned by its `SessionAccess` class and built from the remote's label
|
|
118
|
+
(`Remote.access_error`, keyed by chi web's error code, for any route):
|
|
119
|
+
|
|
120
|
+
| Code | Class | Message |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| 401 `token_revoked` | `TokenRevoked` (an `AuthRefused`) | `this review link was revoked by the session's owner` |
|
|
123
|
+
| 401 `token_expired` | `TokenExpired` (an `AuthRefused`) | `this review link expired: ask the session's owner for a new one` |
|
|
124
|
+
| 403 `not_permitted` | `NotPermitted` | `this token is a participant's: only the session's owner can do that` |
|
|
125
|
+
| 429 `too_many_turns` | `TooManyTurns` (P3b) | `this participant has too many turns waiting: wait for one to finish` |
|
|
126
|
+
| 429 `cap_reached` | `CapReached` (P3b) | `this participant's spending cap is reached: ask the session's owner to raise it` |
|
|
127
|
+
|
|
128
|
+
A 401 `unauthorized` stays `AuthRefused`'s "the token was refused".
|
|
129
|
+
|
|
130
|
+
## Invariants
|
|
131
|
+
|
|
132
|
+
- The owner is a loopback peer or the owner's token; nothing else. A participant token never widens to the owner.
|
|
133
|
+
- A participant reaches one session, through the `:participant` rows only; another session is a 404.
|
|
134
|
+
- No token is written anywhere: the grants store has digests, the sidecar and the audit trail have neither.
|
|
135
|
+
- Owner sessions are unchanged: no sidecar, no audit file, no key in the session JSON until a grant exists.
|
|
136
|
+
- P2 lets no participant reach `POST …/turn`, `/command`, `/answer` or a stream. P3 adds the turn, with the author
|
|
137
|
+
stamped from the principal (never the body), and the worker's review boundary.
|