samagotchi 0.64.0 → 0.66.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 +88 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +20 -3
- data/docs/cli.md +36 -2
- data/docs/configuration.md +177 -8
- data/docs/context.md +4 -1
- data/docs/internals/engine.md +19 -4
- data/docs/internals/prompt-caching.md +10 -2
- data/docs/memory.md +1 -1
- data/docs/plugins.md +163 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +13 -2
- data/docs/sub-agent.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +18 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +21 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +464 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +131 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +68 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +13 -5
- data/lib/samagotchi/engine.rb +151 -11
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +27 -6
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +212 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +80 -7
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +23 -7
- data/lib/samagotchi/session.rb +21 -3
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +8 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +111 -20
- data/lib/samagotchi/web/public/commands.js +4 -2
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +38 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +21 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +16 -9
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +2 -0
- metadata +17 -1
data/docs/internals/engine.md
CHANGED
|
@@ -35,7 +35,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
35
35
|
│ # images (only when there are any), cut_after (the model's cut wait, s)}
|
|
36
36
|
│ # refresh_profile! (may ask the server's /props once)
|
|
37
37
|
│ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name is set in generate)
|
|
38
|
-
│ # announces guardrail load failures, starts and awaits plugin init tasks, applies staged tools
|
|
38
|
+
│ # announces guardrail load failures, starts and awaits plugin init tasks, applies staged tools,
|
|
39
|
+
│ # then the session's toolkit (apply_toolkit!: Registry#toolkit, a prompt rebuild when it changed)
|
|
39
40
|
│ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
|
|
40
41
|
│ # (a frozen copy of the history, the prompt, and resumed)
|
|
41
42
|
│ # turn_messages: system head + history + due reminders (:reminder_injected) + the user message
|
|
@@ -129,8 +130,8 @@ The Engine does not talk to models directly. `Engine#backend` picks the loop the
|
|
|
129
130
|
|
|
130
131
|
```
|
|
131
132
|
Engine#backend → backend_for(@host_registry.resolve(@effective_model_name))
|
|
132
|
-
→ host entry chat? (api: openai): LLM::ChatLoop (one per Engine, built on first use)
|
|
133
|
-
→ else:
|
|
133
|
+
→ host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
|
|
134
|
+
→ else: LLM::NativeBackend wrapping the Engine's KernelLoop
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
Both implement the `LLM::ModelBackend` contract (`#complete(messages:, max_iterations:, on_stream_event:,
|
|
@@ -210,7 +211,10 @@ The differences from the native loop:
|
|
|
210
211
|
- A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
|
|
211
212
|
without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
|
|
212
213
|
`:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
|
|
213
|
-
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step.
|
|
214
|
+
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step. A 400 about a thinking
|
|
215
|
+
block's signature (`BadRequest#thinking_signature?`) or a refused binding control
|
|
216
|
+
(`#thinking_binding_unsupported?`) is not one: `ProviderErrors` leaves `reasoning_refused?` false for them, so one
|
|
217
|
+
bad block never turns reasoning off for the model.
|
|
214
218
|
|
|
215
219
|
### Shared: ToolResponse (per batch) and ToolRunner (per call)
|
|
216
220
|
|
|
@@ -224,6 +228,8 @@ One iteration with tool calls:
|
|
|
224
228
|
→ :tool_dispatch_started {iteration, call_count}
|
|
225
229
|
→ for each call: ToolRunner#run(call, iteration:, call_index:, call_count:,
|
|
226
230
|
on_stream_event:, max_tool_output_chars:)
|
|
231
|
+
→ a tool the session's toolkit leaves out (Registry#left_out) skips the gate: no hooks, no card;
|
|
232
|
+
KernelLoop#dispatch answers it with the not-loaded error
|
|
227
233
|
→ Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
|
|
228
234
|
→ :tool_call_started
|
|
229
235
|
→ an ask verdict: Gate#settle_ask (asks the user, records waited_ms)
|
|
@@ -343,6 +349,10 @@ What the running generation streams, fed by the Engine's stream handler: whether
|
|
|
343
349
|
Bridge acks it as `cut`.
|
|
344
350
|
A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
|
|
345
351
|
the model's cut wait (`SteerCutAfter.resolve`: `steer_cut_after` per models:/hosts: entry, else `steer.cut_after`) seconds (`cuttable?(min_age)`). It has `started!`, `chunk!`, `retrying!` and `finished!`.
|
|
352
|
+
`snapshot` is the running generation as `GenerationPhase::Snapshot` `{thinking_only, age}` (nil with none running;
|
|
353
|
+
`age`: its thinking's seconds, nil before any): `SteerCut#generation_snapshot` → `Engine#generation_snapshot`, which
|
|
354
|
+
`Bridge#snapshot` merges into `current_turn.generation`, so a web page joining mid-generation can clock its delivery
|
|
355
|
+
line and tell whether "cut in now" can land.
|
|
346
356
|
|
|
347
357
|
### WaitingSteer
|
|
348
358
|
|
|
@@ -520,6 +530,11 @@ Both loops:
|
|
|
520
530
|
The retry request runs at temperature 0.6 when the configured sampling sets no temperature; a configured one
|
|
521
531
|
is kept. A length stop with the context at least 90% full is not retried. When the retries run out, the
|
|
522
532
|
Engine keeps a `TurnNote.empty` note as the turn's end.
|
|
533
|
+
- **A refusal** (chat loop only): a finish reason `refusal` (Messages), or `content_filter` (OpenAI) with no text, is
|
|
534
|
+
an empty answer never retried; text streamed before a refusal is not kept, and tool calls with it are not run. A
|
|
535
|
+
`content_filter` with text keeps it, marked `ChatLoop::FILTERED_MARK`, also never retried. `ModelResult#refusal` is `{category:}` (from the response's
|
|
536
|
+
`stop_details`) or `{}`, and goes on the `TurnNote.empty` marker and `turn_summary.empty_answer` as `refusal`, so
|
|
537
|
+
every UI words the notice `no answer: refused by the provider (cyber)`.
|
|
523
538
|
- **A cut generation** (`after_cut`): a generation a plugin cut (`stop_generation`) spends the same retry budget
|
|
524
539
|
with its own nudge; when it is used up, the turn ends cancelled (`:hook`). A steer's cut asks again with the
|
|
525
540
|
steer and spends nothing.
|
|
@@ -22,11 +22,13 @@ right-hand column happens.
|
|
|
22
22
|
```
|
|
23
23
|
shared by every session ────────────────────────────────────────────────── breaks it
|
|
24
24
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
25
|
-
│ tools built-ins, then plugin tools sorted by (bundle, name)
|
|
26
|
-
│
|
|
25
|
+
│ tools built-ins, then plugin tools sorted by (bundle, name), │ a tool added/changed,
|
|
26
|
+
│ those the session's toolkit declares (per toolkit) │ /model, a profile change,
|
|
27
|
+
│ (chat: the `tools` field; native: the model's template) │ a toolkit change
|
|
27
28
|
├──────────────────────────────────────────────────────────────────────────┤
|
|
28
29
|
│ SYSTEM, stable part SystemPrompt#system_prompt_with_index
|
|
29
30
|
│ [native thinking token] base prompt │
|
|
31
|
+
│ not-loaded line (only when the toolkit leaves tools out) │
|
|
30
32
|
│ rg guidance │ /thinking level,
|
|
31
33
|
│ system identity (+ model overlay) │ /model, editing a
|
|
32
34
|
│ model notes (model_notes_* matching the model) │ memory / AGENTS.md
|
|
@@ -57,6 +59,12 @@ right-hand column happens.
|
|
|
57
59
|
- The system prompt is built **once per loop** and rebuilt only on `@prompt_builder.reset!`: a model switch
|
|
58
60
|
(`Engine#switch_model!`), a plugin's `tools_changed!`, or a profile change. A per-turn rebuild would
|
|
59
61
|
break everything below it.
|
|
62
|
+
- The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
|
|
63
|
+
and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
|
|
64
|
+
config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
|
|
65
|
+
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request, so a mid-turn change
|
|
66
|
+
would differ from the turn's prompt. Under `full` nothing is filtered and no line is added: the bytes are what
|
|
67
|
+
they were before toolkits.
|
|
60
68
|
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
61
69
|
breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
|
|
62
70
|
#2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
|
data/docs/memory.md
CHANGED
|
@@ -270,7 +270,7 @@ profiles, meta bundles that install a set of them:
|
|
|
270
270
|
| profile | bundles |
|
|
271
271
|
|---|---|
|
|
272
272
|
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md: the recommended safety set |
|
|
273
|
-
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer |
|
|
273
|
+
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer |
|
|
274
274
|
|
|
275
275
|
`chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
|
|
276
276
|
install gets neither by itself: `chi bundle install core` (or `dev`).
|
data/docs/plugins.md
CHANGED
|
@@ -92,10 +92,14 @@ A line that is not a known command keeps its old meaning: in the terminal
|
|
|
92
92
|
UIs `/foo` goes to the model as a prompt; the web refuses it and names the
|
|
93
93
|
commands it knows.
|
|
94
94
|
|
|
95
|
+
Any whitespace may follow the name, a newline too: `/hello` on its own line
|
|
96
|
+
with text under it runs `/hello` with that text as `args` (its inner lines,
|
|
97
|
+
blank lines and indentation as typed; only the ends are stripped).
|
|
98
|
+
|
|
95
99
|
#### `anytime: true`
|
|
96
100
|
|
|
97
101
|
A plugin's normal command waits for its turn: typed while a turn runs, it is
|
|
98
|
-
refused as busy (the REPL puts it back into the prompt; a plugin command can't
|
|
102
|
+
refused as busy (the REPL puts it back into the prompt, the web into an empty composer; a plugin command can't
|
|
99
103
|
ask to run after the turn, as the built-in `/model X` does). An `anytime: true` command
|
|
100
104
|
runs **at once, on its own thread, beside the running turn**, and the turn
|
|
101
105
|
goes on:
|
|
@@ -144,6 +148,54 @@ chi's own, each bundle's with the bundle's name, and the terminal UIs' own
|
|
|
144
148
|
`attached only`, `repl only`, or `terminal and web only` (`/cut`).
|
|
145
149
|
It works in all three UIs.
|
|
146
150
|
|
|
151
|
+
### `chi.annotation(label, command:, template: "{quote}", send: false)`
|
|
152
|
+
|
|
153
|
+
This adds a pill to the web's selection bar. Selecting text in an answer, a
|
|
154
|
+
thinking block, a step, a tool row or your own message shows
|
|
155
|
+
`[ Annotate | <presets> ┊ <plugin pills> ]`; a plugin pill prepares a command
|
|
156
|
+
line of the plugin from the selection:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
chi.command "/claude", "…", anytime: true do |args, ctx| … end
|
|
160
|
+
chi.annotation "Confirm with Claude", command: "/claude validate"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Clicking it on a thinking line fills the composer with
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
/claude validate
|
|
167
|
+
From your thinking:
|
|
168
|
+
> the quoted line
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
and the caret goes under the quote, for your note; Send runs `/claude` with
|
|
173
|
+
everything after the name as `args`.
|
|
174
|
+
|
|
175
|
+
- `label`: the pill's text, 1–40 characters. Its tooltip adds the bundle.
|
|
176
|
+
- `command:`: `/name` or `/name words`, and `/name` must be a command **this
|
|
177
|
+
plugin** registers (checked once `register` returns, so the order of the
|
|
178
|
+
calls doesn't matter; another one's is a load error). At most 5 per plugin.
|
|
179
|
+
- `template:` goes on the lines under the command line, filled in the
|
|
180
|
+
browser: `{quote}` the labelled quote block Annotate makes, `{text}` the
|
|
181
|
+
selection alone, `{source}` where it came from (`answer`, `thinking`,
|
|
182
|
+
`step`, `tool`, `user`). Another `{x}` stays as written. Each line's
|
|
183
|
+
trailing blanks and the blank lines at either end are dropped, and
|
|
184
|
+
indentation is kept (code stays indented).
|
|
185
|
+
- **Fill** (the default) never sends: an empty composer gets the line; a
|
|
186
|
+
composer that already starts with the same command line gets the quote
|
|
187
|
+
appended (several selections collect under one command); any other draft
|
|
188
|
+
stays, under the command line and the quote.
|
|
189
|
+
- `send: true` sends the line at once (the pill is marked ↗). While a turn
|
|
190
|
+
runs, such a pill is disabled unless its command is `anytime`: it would
|
|
191
|
+
come back busy. A fill pill always works; a typed command refused as busy
|
|
192
|
+
comes back into the composer.
|
|
193
|
+
|
|
194
|
+
Pills ride on their command's listing (the worker's snapshot): a muted
|
|
195
|
+
bundle's go with its command, and a session's pills need a worker started
|
|
196
|
+
after the bundle was installed. Up to 3 show inline, the rest behind `⋯`.
|
|
197
|
+
Web only: the terminals type the command (`/claude validate <text>`).
|
|
198
|
+
|
|
147
199
|
### `chi.tool(name, description, params:, schema:, label:, preview:, targets:, refs:) { |args, ctx| … }`
|
|
148
200
|
|
|
149
201
|
This adds a tool the model can call. It is declared in the system prompt and
|
|
@@ -512,6 +564,21 @@ end
|
|
|
512
564
|
- Each task runs once per session start. A `-p … --non-interactive` run
|
|
513
565
|
starts them with its turn and shows nothing but the answer.
|
|
514
566
|
|
|
567
|
+
### `chi.toolkit(name, base:, add: [], remove: [])`
|
|
568
|
+
|
|
569
|
+
A toolkit sessions can be put on: `base`'s tools (`full`, `lean`, `child`, a
|
|
570
|
+
config or bundle toolkit) with `add` declared and `remove` left out. Users pick
|
|
571
|
+
it like chi's own (`chi --toolkit NAME`, `toolkit.default`, `/toolkit NAME`),
|
|
572
|
+
and the plugin with `ctx.use_toolkit` (below). Names are `a-z`, `0-9`, `_` and
|
|
573
|
+
`-`; one chi has (`full`, `lean`, `child`) or another bundle declared is a
|
|
574
|
+
load error. A session saved on a toolkit whose bundle is gone declares every
|
|
575
|
+
tool (full) until it comes back, with a warning at its first turn. See
|
|
576
|
+
[Toolkits](configuration.md#toolkits).
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
580
|
+
```
|
|
581
|
+
|
|
515
582
|
### `chi.ctx`
|
|
516
583
|
|
|
517
584
|
The plugin's context (the `ctx` its handlers get), for `register` itself:
|
|
@@ -564,6 +631,9 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
564
631
|
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
565
632
|
| `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
|
|
566
633
|
| `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
|
|
634
|
+
| `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
|
|
635
|
+
| `ctx.declared_tools` | the tool names the session's next turn declares (its toolkit resolved, with its own adds and removes): ask this, not the name, whether a tool is there |
|
|
636
|
+
| `ctx.use_toolkit(name)` | put the session on toolkit `name` (yours from `chi.toolkit`, or chi's): saved, applied at the next turn's start and announced (`toolkit> toolkit: NAME from the next turn's start`, the web's tools chip follows). Only before the session's first model reply and outside a turn (the declared tools start every request, and a plugin doesn't ask first); otherwise it returns false and changes nothing: tell the user to run `/toolkit NAME`. An unknown name raises ArgumentError. Needs chi 0.66.0 |
|
|
567
637
|
| `ctx.context` | attach outside text to this session and list it: see [Attached context](#attached-context-ctxcontext) |
|
|
568
638
|
|
|
569
639
|
The Engine itself is never handed to a plugin.
|
|
@@ -1486,6 +1556,13 @@ by themselves there.
|
|
|
1486
1556
|
report ending with the `/coordinate resume <slug>` to pick it up next
|
|
1487
1557
|
time. It starts, merges and stops nothing: running children stay running
|
|
1488
1558
|
and are named in the report.
|
|
1559
|
+
- **The coordinator toolkit.** The bundle declares `coordinator`: `lean`
|
|
1560
|
+
plus `delegate`, `delegate_result` and `list_sessions`
|
|
1561
|
+
([Toolkits](configuration.md#toolkits)). `/coordinate` puts a fresh session
|
|
1562
|
+
on it before it asks the model (`ctx.use_toolkit`). When chi refuses (a reply
|
|
1563
|
+
or a turn already there) and the session's toolkit has no `delegate`, the
|
|
1564
|
+
reply says to run `/toolkit coordinator`. A continue of a coordinator link
|
|
1565
|
+
keeps the toolkit.
|
|
1489
1566
|
- **`/children [all]`** shows the session's children as a card, newest first,
|
|
1490
1567
|
one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
|
|
1491
1568
|
`` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
|
|
@@ -1566,7 +1643,13 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1566
1643
|
file is there (it goes when that Claude session ends), the last send, and
|
|
1567
1644
|
Claude's last answer (the `claude-review` summary).
|
|
1568
1645
|
- `/claude validate <text>` and `/claude context <text>` send the text as it
|
|
1569
|
-
is, titled by its first line
|
|
1646
|
+
is, titled by its first line (past a web quote's `From …:` label and a
|
|
1647
|
+
quoted line's `> `): paste a selection after them.
|
|
1648
|
+
- In the web, selecting text offers **Confirm with Claude**
|
|
1649
|
+
([`chi.annotation`](#chiannotationlabel-command-template-quote-send-false)):
|
|
1650
|
+
it fills the composer with `/claude validate` and the quote, for you to
|
|
1651
|
+
add what needs confirming. On a chi without `chi.annotation` (0.62–0.65)
|
|
1652
|
+
the bundle loads without the pill.
|
|
1570
1653
|
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
1571
1654
|
it is the answer before the running turn, never the turn's partial text;
|
|
1572
1655
|
a cancelled or failed turn has no answer, so the one before it goes.
|
|
@@ -1598,6 +1681,84 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1598
1681
|
minutes). It is local only (a Unix socket), but it does hand session
|
|
1599
1682
|
content to another agent: only what the model or you picked.
|
|
1600
1683
|
|
|
1684
|
+
## The codex-peer bundle
|
|
1685
|
+
|
|
1686
|
+
```sh
|
|
1687
|
+
chi bundle install codex-peer # in the dev profile
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
The [claude-peer bundle](#the-claude-peer-bundle) for an OpenAI Codex CLI
|
|
1691
|
+
session: chi sends the Codex session you work with a part of the work to
|
|
1692
|
+
validate or as context, and Codex's answer comes back into chi. It is a copy
|
|
1693
|
+
of claude-peer with the transport swapped; what differs is below, the rest
|
|
1694
|
+
(kinds, payload files, the last send, `/codex last`, the status card, the
|
|
1695
|
+
skill install card) works the same.
|
|
1696
|
+
|
|
1697
|
+
- **Sending is `codex queue`.** `codex_send(kind, title, text)` and
|
|
1698
|
+
`/codex validate|context <text>` run
|
|
1699
|
+
`codex queue --thread <uuid> --message <text>` (an argv, no shell), which
|
|
1700
|
+
wakes an idle Codex TUI: the message runs there as a new user turn (checked
|
|
1701
|
+
on Codex 0.162.1; a message queued mid-turn runs after the turn). The
|
|
1702
|
+
thread is the `codex-inbox` context's hint, Codex's `CODEX_THREAD_ID`, which
|
|
1703
|
+
must look like a UUID. `codex` is `$CODEX`, else `codex` on the PATH: a
|
|
1704
|
+
worker started by `chi web` or launchd may have another PATH than your
|
|
1705
|
+
shell, so set `CODEX=` where chi runs if the error says there's no codex.
|
|
1706
|
+
The tool and the anytime `/codex` wait for `codex queue` up to 10 s; a
|
|
1707
|
+
queue that takes longer is killed (its process group) and the send fails
|
|
1708
|
+
with a timeout error, never a hang. Other errors say what to do: no
|
|
1709
|
+
`codex-inbox` (pair from Codex), a thread Codex has no record of (that
|
|
1710
|
+
Codex session is gone: pair again), an ephemeral thread
|
|
1711
|
+
(`codex exec --ephemeral` can't take queued messages), or `codex queue`'s
|
|
1712
|
+
first error line. A Codex session that was closed (`/quit`) is not gone:
|
|
1713
|
+
Codex's app-server still takes the queue (exit 0), chi says sent, and no
|
|
1714
|
+
answer comes; the message waits for that session (`codex resume` there, or
|
|
1715
|
+
pair again from a new one). The status card shows the thread id and says
|
|
1716
|
+
so; there is no cheap check that the session is open.
|
|
1717
|
+
- **Codex's sandbox.** Pairing and every reply write chi's state
|
|
1718
|
+
(`chi context add`/`push` write the session's context files directly),
|
|
1719
|
+
which Codex's default sandbox (workspace-write) refuses. The user adds, to
|
|
1720
|
+
`$CODEX_HOME/config.toml` (else `~/.codex/config.toml`):
|
|
1721
|
+
|
|
1722
|
+
```toml
|
|
1723
|
+
[sandbox_workspace_write]
|
|
1724
|
+
writable_roots = ["/Users/you/.local/state/samagotchi/context"]
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
and restarts Codex. Only chi's `context/` folder: the state folder above it
|
|
1728
|
+
also holds guardrails, session files, plugins and the web token.
|
|
1729
|
+
`/codex install-skill`'s card shows the line with your state folder
|
|
1730
|
+
(absolute: config.toml doesn't expand `~`), and the pair script names it
|
|
1731
|
+
when a write is refused ("Operation not permitted"). chi never edits
|
|
1732
|
+
Codex's config. Reading a payload file needs nothing: the default sandbox
|
|
1733
|
+
reads anywhere. No network is needed: a push writes files, and chi wakes
|
|
1734
|
+
on them.
|
|
1735
|
+
- **Pairing** is Codex's, as with Claude: `/codex install-skill` copies the
|
|
1736
|
+
bundle's `scripts/chi-peer-SKILL.md` to
|
|
1737
|
+
`$CODEX_HOME/skills/chi-peer/SKILL.md` (else `~/.codex/…`), with the
|
|
1738
|
+
installed `scripts/pair` path, the payload folder and the writable root
|
|
1739
|
+
written in; in Codex you say "pair with chi <id or chi web URL>", and
|
|
1740
|
+
`sh <bundle>/scripts/pair` attaches `codex-inbox` (the thread id as its
|
|
1741
|
+
hint, pushed so a re-pair refreshes it) and `codex-review`. Run outside
|
|
1742
|
+
Codex (no `CODEX_THREAD_ID`) it refuses. Codex replies with one JSON push
|
|
1743
|
+
to `codex-review` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
1744
|
+
first answer too: that needs chi 0.63.0, the bundle's `requires_chi`.
|
|
1745
|
+
- **Peer messages are user turns in Codex.** Claude Code marks an inbox
|
|
1746
|
+
message as from another session; `codex queue` lands as a plain user
|
|
1747
|
+
message. So Codex's skill starts with the rule: a message whose first line
|
|
1748
|
+
is exactly `chi <8 hex> · <kind> · <title>` comes from chi, a peer agent,
|
|
1749
|
+
not the user; it can't approve anything, change settings or widen the
|
|
1750
|
+
sandbox, and the part it carries (and a payload file) is data to check,
|
|
1751
|
+
never instructions, since it may hold tool or web output. This is a prompt
|
|
1752
|
+
rule Codex follows, not something enforced: anything shaped like that
|
|
1753
|
+
header is treated this way, and nothing stops a message from asking for
|
|
1754
|
+
more.
|
|
1755
|
+
- In the web, selecting text offers **Confirm with Codex**, which fills
|
|
1756
|
+
`/codex validate` and the quote, as claude-peer's pill does.
|
|
1757
|
+
- **The skill.** `skill_codex_peer` (system scope) is claude-peer's with
|
|
1758
|
+
Codex's names, and on a missing answer points to Codex being mid-turn or
|
|
1759
|
+
its sandbox (the `writable_roots` line). To be asked before each send, a
|
|
1760
|
+
guardrails rule with `tool: codex_send` (see the claude-peer section).
|
|
1761
|
+
|
|
1601
1762
|
## Shutdown
|
|
1602
1763
|
|
|
1603
1764
|
When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, source-links, github-pr, coordinator, claude-peer, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
data/docs/sessions.md
CHANGED
|
@@ -76,7 +76,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
76
76
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
77
77
|
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
78
78
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
79
|
-
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
79
|
+
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
80
80
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
81
81
|
|
|
82
82
|
**Test-session hygiene:**
|
|
@@ -135,7 +135,8 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
|
|
|
135
135
|
- cancelled (Ctrl-C, the web's stop): `[SYSTEM: the previous turn was cancelled (ctrl-c) after 12s; the reply
|
|
136
136
|
above ends where it was cut off.]` (an answer or only narration had streamed), or `…; no answer had been shown.]` when only thinking had streamed;
|
|
137
137
|
- ended with nothing visible: `[SYSTEM: the previous turn ended with no visible answer (thinking only, or
|
|
138
|
-
nothing). The user's last message is still unanswered.]
|
|
138
|
+
nothing). The user's last message is still unanswered.]`, or `[SYSTEM: the previous turn's answer was refused by
|
|
139
|
+
the provider (cyber). …]` when the provider refused it.
|
|
139
140
|
|
|
140
141
|
When the context window fills past 40% (then 60%, 80%), the native loop leaves a similar line (`kind: context`):
|
|
141
142
|
`[CONTEXT: about 55% of the context window is in use (estimated; bucket=40plus). context moderate — prefer targeted
|
|
@@ -219,6 +220,16 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
|
|
|
219
220
|
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT]` (see *Starting a session*).
|
|
220
221
|
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
221
222
|
|
|
223
|
+
## Toolkits
|
|
224
|
+
|
|
225
|
+
A session file's `toolkit` (`{"name", "add", "remove"}`) is the toolkit it was created on, as named: config's
|
|
226
|
+
`toolkit.default`, `toolkit.child` for a delegate child, or `chi --toolkit` / `chi send --new --toolkit` (with
|
|
227
|
+
`--tool` / `--no-tool`). It is never resolved again from config: a respawned or woken worker declares the same
|
|
228
|
+
tools. `null` (a session from before toolkits) is `full`. A continue (`continue →`, `chi send --new --continues`)
|
|
229
|
+
and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets `toolkit.child`.
|
|
230
|
+
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
231
|
+
[Toolkits](configuration.md#toolkits).
|
|
232
|
+
|
|
222
233
|
## Delegating
|
|
223
234
|
|
|
224
235
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
data/docs/sub-agent.md
CHANGED
|
@@ -17,7 +17,9 @@ The other direction, chi sending a part of its work to a Claude Code session
|
|
|
17
17
|
you already work with (to validate, as context, or with a question), is the
|
|
18
18
|
[claude-peer bundle](plugins.md#the-claude-peer-bundle): Claude pairs with the
|
|
19
19
|
chi session, chi's `claude_send` tool or `/claude` writes to Claude's inbox,
|
|
20
|
-
and Claude's answer comes back as attached context.
|
|
20
|
+
and Claude's answer comes back as attached context. The
|
|
21
|
+
[codex-peer bundle](plugins.md#the-codex-peer-bundle) does the same with a
|
|
22
|
+
Codex CLI session (`codex_send`, `/codex`, through `codex queue`).
|
|
21
23
|
|
|
22
24
|
## The commands
|
|
23
25
|
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -254,7 +254,7 @@ module Samagotchi
|
|
|
254
254
|
seq = @engine.event_count
|
|
255
255
|
{
|
|
256
256
|
messages: @engine.messages_checkpoint,
|
|
257
|
-
current_turn:
|
|
257
|
+
current_turn: current_turn_with_generation,
|
|
258
258
|
queued: @accumulator.queued,
|
|
259
259
|
queued_commands: @accumulator.queued_commands,
|
|
260
260
|
recap: @accumulator.recap,
|
|
@@ -316,6 +316,16 @@ module Samagotchi
|
|
|
316
316
|
|
|
317
317
|
private
|
|
318
318
|
|
|
319
|
+
# The turn in progress (TurnAccumulator#current_turn) with the running
|
|
320
|
+
# generation's phase as generation: {thinking_only:, age:}
|
|
321
|
+
# (Engine#generation_snapshot), so a page joining mid-generation can
|
|
322
|
+
# clock its delivery line and say whether a cut can land.
|
|
323
|
+
def current_turn_with_generation
|
|
324
|
+
turn = @accumulator.current_turn
|
|
325
|
+
gen = turn && @engine.generation_snapshot
|
|
326
|
+
gen ? turn.merge(generation: gen.to_h) : turn
|
|
327
|
+
end
|
|
328
|
+
|
|
319
329
|
# The Engine's commands: the built-ins and its plugins'.
|
|
320
330
|
def command_registry
|
|
321
331
|
@engine.command_registry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.2.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-review context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -10,7 +10,7 @@ files:
|
|
|
10
10
|
description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks; validate, context or question; Claude's answer comes as the claude-review context"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
13
|
+
sha256: sha256:ee44f119d6f8c6cb25917a0f3fb2cfd1fbca424d537ee5fa7043594a9eb04098
|
|
14
14
|
scripts:
|
|
15
15
|
pair: sha256:033f1fc6a644950b57e71865253526a0a487bb4c9fd5a61bb89d031b65f40417
|
|
16
16
|
chi-peer-SKILL.md: sha256:f35f726b1a86091ff69d289e8dd58794ac6f44879a63630fcc88d69867c56e20
|
|
@@ -42,6 +42,12 @@ require "time"
|
|
|
42
42
|
# being the user's consent: the copy happens in-process, where no guardrail
|
|
43
43
|
# sees it. The skill is scripts/chi-peer-SKILL.md beside this file, with the
|
|
44
44
|
# installed scripts/pair path and the payload folder written in.
|
|
45
|
+
#
|
|
46
|
+
# In the web, selecting text offers "Confirm with Claude" (chi.annotation;
|
|
47
|
+
# skipped on a chi without it, 0.62–0.65): it fills the composer with
|
|
48
|
+
# `/claude validate` and the quote, for the user to add what to confirm.
|
|
49
|
+
# Such a text is titled by its first quoted line, not the quote's
|
|
50
|
+
# `From your thinking:` label.
|
|
45
51
|
class Plugin
|
|
46
52
|
INBOX = "claude-inbox"
|
|
47
53
|
REVIEW = "claude-review"
|
|
@@ -70,6 +76,8 @@ class Plugin
|
|
|
70
76
|
PAIR_PLACEHOLDER = "__CHI_PEER_PAIR__"
|
|
71
77
|
PAYLOADS_PLACEHOLDER = "__CHI_PEER_PAYLOADS__"
|
|
72
78
|
DIFF_LINES = 20
|
|
79
|
+
# The label line a web quote starts with (annotations.js sourceLabel).
|
|
80
|
+
QUOTE_LABEL = /\AFrom (?:your thinking|your earlier step|the .+ call|my earlier message):\z/
|
|
73
81
|
|
|
74
82
|
TOOL_DESCRIPTION = "Send a part of this session's work (a plan step, a diff, a finding, your last answer) to the " \
|
|
75
83
|
"Claude Code session paired with it (the claude-inbox context), for Claude to validate, to take " \
|
|
@@ -111,6 +119,8 @@ class Plugin
|
|
|
111
119
|
"install-skill: install Claude's chi-peer skill", anytime: true do |args, ctx|
|
|
112
120
|
claude(args.to_s.strip, ctx)
|
|
113
121
|
end
|
|
122
|
+
# A chi without chi.annotation (0.62–0.65) loads the bundle without the pill.
|
|
123
|
+
chi.annotation "Confirm with Claude", command: "/claude validate" if chi.respond_to?(:annotation)
|
|
114
124
|
chi.on(:before_turn) do |event|
|
|
115
125
|
@lock.synchronize { @before_turn = Array(event[:messages]) }
|
|
116
126
|
end
|
|
@@ -167,12 +177,19 @@ class Plugin
|
|
|
167
177
|
end
|
|
168
178
|
|
|
169
179
|
def send_text(ctx, kind, text)
|
|
170
|
-
"#{deliver(ctx, kind: kind, title: text
|
|
180
|
+
"#{deliver(ctx, kind: kind, title: text_title(text), text: text)}; Claude's answer comes as the " \
|
|
171
181
|
"claude-review context"
|
|
172
182
|
rescue SendError => e
|
|
173
183
|
"/claude: #{e.message}"
|
|
174
184
|
end
|
|
175
185
|
|
|
186
|
+
# A typed text's title: its first line, past a web quote's label line
|
|
187
|
+
# (`From your thinking:`) and the `> ` of a quoted line.
|
|
188
|
+
def text_title(text)
|
|
189
|
+
lines = text.strip.lines.map(&:strip).reject { |line| line.match?(QUOTE_LABEL) }
|
|
190
|
+
lines.map { |line| line.sub(/\A>\s*/, "") }.find { |line| !line.empty? }.to_s
|
|
191
|
+
end
|
|
192
|
+
|
|
176
193
|
def send_last(ctx)
|
|
177
194
|
answer, before = @lock.synchronize { [@last_answer, @before_turn] }
|
|
178
195
|
answer ||= completed_answer(before || ctx.messages)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-peer
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Pair this session with an OpenAI Codex CLI session you work with: the codex_send tool and /codex send it a part of the work (a plan step, a diff, a finding) to validate or as context, with codex queue, and Codex's answer comes back as the codex-review context; /codex install-skill installs Codex's chi-peer skill, which pairs"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
files:
|
|
8
|
+
skill_codex_peer.md:
|
|
9
|
+
sha256: sha256:b17b97830d7895ca6bf393fe040d71a2eacc2e0b68fa9a23ba35d44f39132493
|
|
10
|
+
description: "Send a part of the work to the paired Codex CLI session (codex_send): only when the user asks; validate, context or question; Codex's answer comes as the codex-review context"
|
|
11
|
+
plugin:
|
|
12
|
+
file: plugin.rb
|
|
13
|
+
sha256: sha256:365382abe8f9f72e82eb965b762969e64c3774e3279d1e9cdc63a3f7286eb76f
|
|
14
|
+
scripts:
|
|
15
|
+
pair: sha256:90cc286587260a4221c6ca9621cf5f34dd2ac42dc1e6393b69ba3e974806e695
|
|
16
|
+
chi-peer-SKILL.md: sha256:886b6c430e9678f7f8bd991bdb374b6146ca2cbb0f42e36865ab14f5a21fa6e0
|
|
17
|
+
# Codex's answer is a pushed source's first wake: true push, which wakes the
|
|
18
|
+
# session from chi 0.63.0 (before, the first answer arrives as a note
|
|
19
|
+
# without waking); ctx.context.list's summary: (the /codex card's last
|
|
20
|
+
# answer) is 0.63.0 too.
|
|
21
|
+
requires_chi: ">= 0.63.0"
|