samagotchi 0.160.0 → 0.161.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 +86 -1
- data/docs/architecture.md +7 -3
- data/docs/cli.md +6 -4
- data/docs/context.md +10 -1
- data/docs/guardrails.md +12 -3
- data/docs/internals/bridge.md +21 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +4 -2
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +37 -5
- data/docs/sessions.md +14 -11
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +92 -42
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +56 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +2 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/engine/events.rb +12 -0
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +59 -12
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installer.rb +79 -12
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +2 -1
- data/lib/samagotchi/memory_bundle/status.rb +1 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +27 -13
- data/lib/samagotchi/memory_orphans.rb +40 -9
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/project_init.rb +3 -3
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +286 -48
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +42 -5
- data/lib/samagotchi/session_access/local.rb +45 -17
- data/lib/samagotchi/session_access/remote.rb +11 -4
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +16 -3
- data/lib/samagotchi/session_continue.rb +155 -21
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +164 -104
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +18 -7
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/delegate.rb +9 -4
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +7 -2
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +12 -8
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +45 -7
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +41 -8
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3a8c4d1d4d37965fda091bc5c48281cdf352d6f3e496d5e5073f72777fe5994
|
|
4
|
+
data.tar.gz: 74a26cfde1a9028c44763b99cdc60ce044028598ddd2955b244338076ef8fd86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1de962941edd2efc63a07e566954a8e6cd212891c4858db791667341e0eca601b0e920a4661198760317f5133ee26e3411d9cad570a63a87e861527ab8e21a7e
|
|
7
|
+
data.tar.gz: 0fb530ff96ccbfe79f2dc04b4cba6d59fc1b0ee6a921cf593907d9159b60eec1bed10c3360f1777417f30333c9afd36090d2b3abf7f58b9e4e4cf1b27e558946
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,90 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.161.0] - 2026-11-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `chi bundle upgrade NAME` and `chi bundle uninstall NAME`, when the bundle's plugin changed (or an upgrade brought
|
|
16
|
+
one in) and a session running it can be restarted, end with one line naming those sessions and
|
|
17
|
+
`chi sessions restart ID` for each (nothing on a dry run, with no restartable worker, or when the plugin is
|
|
18
|
+
unchanged — only the plugin stays in a worker's memory; its rules reload by stamp and its scripts run from disk).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Worker restart hints now follow one compatibility rule: the current and previous minor release are supported.
|
|
23
|
+
- `chi bundle install` and `chi bundle upgrade` refuse a memory, plugin or script whose bytes disagree with the
|
|
24
|
+
sha256 its manifest declares, before writing anything (`Checksum mismatch for FILE: ...; chi bundle install
|
|
25
|
+
--force installs it anyway`, exit 1). `--force` installs it anyway, as it already does for an edited memory (its
|
|
26
|
+
copy going to the trash first) and for a bundle whose `requires_chi` this chi doesn't meet; a dev install, a
|
|
27
|
+
non-strict run, `chi update` and a bundle profile keep the warning-only behaviour.
|
|
28
|
+
- Core guardrail checks judge a plugin tool by what its `targets:` say it acts on, not by its name: paths it
|
|
29
|
+
declares it writes (`writes: true`) are checked like `write` and `edit`. See docs/plugins.md (Guardrails).
|
|
30
|
+
- `chi context push` says what a wake push did: `· chi woken` (an idle-exited session started), `· chi is stopped:
|
|
31
|
+
it sees this at its next start` (a stopped session is never started) or `· chi is starting`; chi web's push
|
|
32
|
+
answer carries it as `wake`. A delegate's report, or a push, now wakes a session whose file still said stopped
|
|
33
|
+
without a `chi sessions stop` behind it: only the stop marker means stopped.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- A session being started is no longer pulled from under its worker: a session resumed while the retention sweep runs
|
|
38
|
+
is not deleted, two starts at once (chi web and a `chi send`, a delegate's report) start one worker, and a second
|
|
39
|
+
continue of a chain can't archive the link the first one just started. A deleted session no longer comes back
|
|
40
|
+
when a worker that still had it saves.
|
|
41
|
+
- A `chi sessions stop` that lands while a session's worker is leaving (an idle exit, `/exit`, a signal) is no longer
|
|
42
|
+
undone by a new worker started for input or delegate reports that were waiting: the session stays stopped and
|
|
43
|
+
they wait for its next start, as a stop during a restart already did.
|
|
44
|
+
- Session deletion and retention now also end participant grants and purge System 1 runs and context-candidate records.
|
|
45
|
+
- An interrupted continue now repairs its archived previous link when no successor was saved, so the session stays listed.
|
|
46
|
+
- A note sent to a session that was continued (`chi note`, `send_note`, `chi broadcast`, chi web) reaches the new link
|
|
47
|
+
(`chi note` prints `OLD → NEW (continued)`), and notes waiting when it was continued move with it; a note that
|
|
48
|
+
still lands in the old link as it is continued is taken by the new link's worker, and a continue that fails gives
|
|
49
|
+
the notes back. An `/objectives` edit stays in its own session.
|
|
50
|
+
- `chi bundle uninstall NAME` of a bundle whose install was interrupted before its record (only the
|
|
51
|
+
`pending.json` journal left) retags the index line of a kept shared file in the journal's own target folder, not the
|
|
52
|
+
current folder's: run from another project, it no longer edits that project's `index.md` for a file living elsewhere.
|
|
53
|
+
- A schedule counts a firing only after its turn completes: failed, canceled or unanswered turns now count toward
|
|
54
|
+
`max_failures` with the reason visible in `chi schedule list` and `show`.
|
|
55
|
+
- A schedule firing left without a turn after a process interruption is recorded as a failure instead of staying
|
|
56
|
+
stuck; its uncertain occurrence is not replayed.
|
|
57
|
+
- `chi schedule run-now` reserves an entry under the tick lock before firing, preventing an overlapping tick from
|
|
58
|
+
starting the same work.
|
|
59
|
+
- Retention protects an enabled schedule's stored anchor and head as well as its resolved target; cancel the
|
|
60
|
+
schedule with `chi schedule cancel ID` before deleting any of them.
|
|
61
|
+
- Schedule add, list and `schedule_task` warn when the ticker is stale and auto-tick is off; acquired ticks record
|
|
62
|
+
their timestamp.
|
|
63
|
+
- chi web's schedule tick runs outside the session scan loop, and overlapping hub scans do not start a second
|
|
64
|
+
tick while one is in flight.
|
|
65
|
+
- The web chain chip waits until its session is in the loaded list before saying earlier links were pruned.
|
|
66
|
+
- A `chi send`, delegate or schedule delivery whose Bridge answered an error after it had queued the prompt no
|
|
67
|
+
longer runs it twice: the client makes the enqueued id, the input-file fallback reuses it, and the worker runs
|
|
68
|
+
each id once (also after a restart, a turn that already ran or was cut off by the worker's death).
|
|
69
|
+
- The continue's carry note of unabsorbed pushed context keeps the sources after one trimmed to the per-source 4 KiB
|
|
70
|
+
cap: a source that fits the room left is carried whole even when that room is under the cap, the note ends only at a
|
|
71
|
+
source it has no room for, and "[additional pushed text omitted]" is added whenever text is left out.
|
|
72
|
+
- The memory-orphan hint says how the other folder is keyed: a `chi project init` folder is named by its project id
|
|
73
|
+
instead of "another path's hash", and a project keyed by an id is named by its folder, not by that id (so a marked
|
|
74
|
+
folder whose memories stayed in its id folder after `git init` is described correctly).
|
|
75
|
+
- A browser tab or terminal that connects to a worker with no cursor, after its event ring has wrapped, is re-synced
|
|
76
|
+
from the session instead of replaying from the middle of a turn.
|
|
77
|
+
- Stop (the web button, Ctrl-C in the terminal) names the turn it was pressed for, so one pressed as that turn ends no
|
|
78
|
+
longer cancels the queued turn that starts next.
|
|
79
|
+
- `chi bundle install` and `upgrade` check that the plugin and scripts the manifest names are in the bundle before
|
|
80
|
+
they write anything, so a bundle that lacks one is refused with your memories, plugin and record as they were.
|
|
81
|
+
- A bundle install or upgrade that is killed or fails after it copied memories (a `pending.json` journal in
|
|
82
|
+
`.bundles/NAME/`) no longer leaves them as your own files or local edits: a retry owns them, `chi bundle status`
|
|
83
|
+
shows no edit, the next upgrade fast-forwards them and uninstall removes them.
|
|
84
|
+
- A `delegate` follow-up the child's worker refuses says why, in the same sentence `chi send` prints for a delivery
|
|
85
|
+
that did not go in (`DeliverResult#refusal_text`): "Error: session abc12345 refused: this session's worker predates
|
|
86
|
+
queueing ... and send again" rather than the bare error code.
|
|
87
|
+
- A fork and a continue keep the session's `--memory` and `--mute` lists; a non-delegate child does not inherit the
|
|
88
|
+
reserved `system/delegated` memory.
|
|
89
|
+
- A review fork carries its source's `--mute` list, but not its `--memory` preloads.
|
|
90
|
+
|
|
91
|
+
Update with `chi update`, then restart chi web and running sessions (`chi sessions restart`): workers from
|
|
92
|
+
0.160.0 keep working (N-1), but the new starting marker, session inbox and stop-during-leave rule apply only to
|
|
93
|
+
workers started by 0.161.0.
|
|
94
|
+
|
|
11
95
|
## [0.160.0] - 2026-11-09
|
|
12
96
|
|
|
13
97
|
### Added
|
|
@@ -5922,7 +6006,8 @@ and long-lived sessions.
|
|
|
5922
6006
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5923
6007
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5924
6008
|
|
|
5925
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
6009
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.161.0...HEAD
|
|
6010
|
+
[0.161.0]: https://github.com/dm1try/samagotchi/compare/v0.160.0...v0.161.0
|
|
5926
6011
|
[0.160.0]: https://github.com/dm1try/samagotchi/compare/v0.159.0...v0.160.0
|
|
5927
6012
|
[0.159.0]: https://github.com/dm1try/samagotchi/compare/v0.158.0...v0.159.0
|
|
5928
6013
|
[0.158.0]: https://github.com/dm1try/samagotchi/compare/v0.157.0...v0.158.0
|
data/docs/architecture.md
CHANGED
|
@@ -193,10 +193,14 @@ log's last 4 MB). Rows: [configuration.md](configuration.md#checking-it-chi-home
|
|
|
193
193
|
|
|
194
194
|
## Session retention & ordering
|
|
195
195
|
|
|
196
|
-
- **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files. Durable schedules belong to the machine, not a session: `~/.local/state/samagotchi/schedules.json` and its `schedules.lock`, next to `sessions/` ([Scheduling](scheduling.md)).
|
|
196
|
+
- **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped`, `<uuid>/starting` and `<uuid>/archived` marker files. Durable schedules belong to the machine, not a session: `~/.local/state/samagotchi/schedules.json` and its `schedules.lock`, next to `sessions/` ([Scheduling](scheduling.md)).
|
|
197
197
|
- **Single owner:** the process running a session's Engine (its worker, or a `--non-interactive` one-shot) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a session a one-shot owns.
|
|
198
|
-
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
|
|
199
|
-
- **
|
|
198
|
+
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file. A resume writes nothing else: the worker, holding the lock, turns a status from before it idle (`Worker#settle_stale_status`: `stopped` with no marker, `error`, a `running` no turn is behind; not a new session's `running` whose first prompt waits), and saves at start whatever the status, which stamps `updated_at` as the resume's save did (a session woken with no turn stays young for the sweep). N-1: a newer spawner starting an older worker (a checkout's chi spawning the installed one) loses that reset; a restart covers it.
|
|
199
|
+
- **Starting:** every spawn marks the session starting (`<uuid>/starting`, `{pid, at}`, `StartingMarker`) until its worker holds the lock: `spawn_worker_for_session` writes it with its own pid before `Process.spawn` and the starter's after (the starter lives until the worker holds the lock, at most `WORKER_START_GRACE`; a spawn that raises or whose starter is gone leaves none), `spawn_session` before the new file is saved, and `Worker#start` removes it. It holds for 60 s at most, and only while the pid it names runs. A resume claims it (`StartingMarker.claim`, a link(2): of resumes racing from any processes one wins), then reads the owner again. A stop marker cancels the hold-off (`start_pending?` = starting and not stop-marked) for a start marked before the stop only: of two sends after a stop, the claim lets one spawn. The stop marker carries its own `at` (`{"at"}`, on the starting marker's clock; `Session.stopped_at`), an older chi's empty one its mtime. The sweep, archive (`:starting`) and delete (refused as an owned session) honour it. N-1: an older spawner writes none for a resume (the owner checks stay); an older worker never leaves one it was given (every worker clears it at start); a leftover one is stale by age or pid (a claim's temp files a kill left, `starting.*`, count as skeleton); a 0.160 spawner or chi web still holds off on a fresh marker under a stop (it reads `starting?` before the stop), until the start ends or 60 s. `SessionManager` keeps only its starters' pids, for chi web's restart to adopt (`live_spawn_pids`), and decides nothing from them.
|
|
200
|
+
- **Deleting:** `delete_session`, the retention sweep and a failed continue's cleanup use `remove_files` for the session JSON, sidecar dir, plugin state, attached context, participant grants, System 1 runs and context-candidate records. The JSON and dir go through `remove_session_files`, holding the owner lock (kind `deleting`, which `stop_session` never signals; not waited for: a worker that took it since the check keeps the session) and reading the starting marker again under it (a resume that claimed the start just before keeps the session; one after it sees the lock held). N-1: an older chi's stop reading the owner then would TERM the deleter (chi web, the CLI) in that moment; an older reader shows the kind as unknown. `Session#save` of a session read from its file (or saved once) skips a file that is gone (log `save_skipped_deleted`) rather than recreate it; only a new session's first save creates one.
|
|
201
|
+
- **Worker presence:** whether a worker is up, coming or held off is read once per decision as a `WorkerPresence` (`worker_presence.rb`, a `Data.define`), the deciders' one reader of those facts (display and list rows still read `session_owner` / `WorkerLink` themselves): the owner lock's holder, the stop marker (`stop_marked?`: the only "stopped" a decider reads, never the session file's status), the `starting` marker (`StartingMarker`, `starting?`, `start_pending?`: every spawn writes it, see Starting), the archive marker, the session file's existence, and the `WorkerLink` outcome when asked (never the file itself: a scratch session's flag is read by `WakePolicy`, last). The deciders read it: `resume_session` (tui, owned, `resume_held_off?`), the session inbox's `WakePolicy` (a ring, a push's wake, a note, a fresh verdict, `wake_for_report`, `deliver_turn`'s two liveness checks, `WorkerBoot`'s leave: below), `Local#wake`/`#worker`, `AttachLauncher`, `Schedules#busy_reason`, archive, delete and the retention sweep. Each keeps its own subset of the facts and their order.
|
|
202
|
+
- **Session inbox:** a delivery into a session (`SessionInbox.deliver(kind, id) { |at| write }`, `session_inbox_delivery.rb`) runs one path: resolve the link (`SessionInbox::Head.resolve`: send, push and a fresh verdict are pinned to the id named; a note goes to the chain's newest link; a ring's file stays with the parent `parent.json` names and its wake goes one hop, to the link whose `ChildReports` reads that file; only an archived id is walked, with one `Session.list`), refuse before any write (a gone session; a one-turn run's or a scratch session for send and a note), write with the kind's own writer (formats unchanged), read `WorkerPresence` once, and apply the kind's `WakePolicy` row. `decide` reads in one order for every kind: gone, one-turn run (`:owned`), owned (`:live`), the stop marker (`:stopped`: the only "stopped", the file's status never decides; send un-stops), archived (send alone starts one), starting (`:starting`, never a second spawn), the kind never starts (`:none`: a note, a fresh verdict) or wasn't asked to (`:off`), scratch (the session file is read only here), else `:start` through `SessionManager.start_for_inbox` (`resume_session`, its claim on the starting marker; it answers `:woken` only when it spawned the worker, `:starting` when another start won the claim, `:live` when a worker took the lock since). N-1 gap: a session file that says `stopped` with no stop marker (stopped before the marker existed, or a resume whose spawn failed after it cleared the marker) is woken, and an older worker, which has no `Worker#settle_stale_status`, exits on its first pass; the wake still reads `:woken`, and each later ring or push spawns another such worker (bounded by the rings and pushes). Restarting chi on the new version (`chi web`, the workers) closes it. `SessionInbox.wake` is the wake half alone (`wake_for_report`, a failed continue's rings). On it: `ChildRing.ring` (its start on a thread), `Local#context_push`'s wake (its outcome is `ContextPush#wake`, the push line's ending), `NoteDelivery.deliver` (every note writer a person or another session drives: `chi note`, `send_note`, `chi broadcast`, chi web's notes route; `NoteDelivery::Result#id` is the link that has it, `#asked` the one named; `/objectives` passes `pin: true`: its list is its own session's) and `VerifyFresh.forward` (`:fresh_verdict`: the child's parent, pinned, no worker). Send keeps its Bridge/file flow in `SessionManager.deliver_turn` (pinned to the id it names: an archived link gets the message and comes back); its two liveness checks (before the post, after a file's write) read the `:send` row and leave the refusals to `resume_session`'s wording. One row is not a delivery: `:leave`, `WorkerBoot.run_session_loop`'s successor as a worker leaves (input left after a signal, an idle exit's or `/exit`'s input or rings, a restart), read on a fresh presence once the lock is free: a stop that landed as it left starts none for any way out (`resume_session` would clear the stop; logged `respawn_dropped`, `restart_dropped` for a restart, whose reminders go with it), the input and rings stay for the next start. The notes a spawn writes into the session it starts stay direct `SessionInbox.write_note` calls, pinned by construction: spawn_session's chain note, `ContextMove`'s notes, `Objectives.carry`, `ChildMove`'s note to a moved child. A continue moves the previous link's waiting notes to the new link last in its `before_spawn` (`NoteMove.move`), a failed one gives them back before the link goes (`NoteMove.give_back`), and the new link's worker adopts what the archived links before it get after their continue (`NoteMove.adoption`, from `WorkerInbound#absorb_notes` and `Worker#start`: back along the chain through up to 8 archived links, from each only notes from the creation of the link that continued it, never an objectives list). `write_note` refuses a session with no file and no folder (`Session::NotFound`), so a late writer can't bring back a deleted link's folder.
|
|
203
|
+
- **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner and starting guard (looked at again right before a session's files go), only when `*.json` present; lazy sweep at most once per `session.sweep_interval_hours` (default 24) from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls), manual via `bin/chi sessions prune --dry-run`.
|
|
200
204
|
- **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
|
|
201
205
|
- **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
|
|
202
206
|
|
data/docs/cli.md
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
- `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, and check the bundles from elsewhere, in one table (see [Updating](#updating))
|
|
38
38
|
- `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
|
|
39
39
|
- `chi project`, `chi project init [--new-id] [DIR]` — this folder's project, and making a folder in no git repository one. `chi project` prints how its project was found (`kind` `git`, `marker` or `none`), its `root`, its `top` (where the AGENTS.md chain starts), a marked project's `id` (what its memories and project context are keyed on; else the root's path is) and its project `memories` folder, with a `note` for a marker a repository makes ignored, or a `$HOME` that is a git repository. `init` writes `DIR/.chi/project.yml` with a new `id:` (DIR defaults to this folder and is created when missing), so DIR and every subfolder share one memories folder (kept when DIR moves or is renamed), group their sessions and take `chi context --project`; it writes nothing in a git repository (the repository already is the project, exit 0), says so (with the id) for a folder already marked, refuses `$HOME`, a folder above it and `/` (exit 1), and marks a folder inside another marked project as its own, nested one. On a folder marked without an id (an older chi's marker) it adopts one: the folders keyed by its path move to the id, each leaving a link by its old name, and it says what moved and names memories an earlier path left (refused, exit 1, while a session of the project runs). `--new-id` gives a marked folder a fresh id with a copy of its memories and project context (a copied folder going its own way; refused elsewhere, exit 1). It prints the root, the id and the memories folder (with the memories already there: a folder chi ran in at its top keeps them, moved to the id), the running sessions in DIR to restart (`chi sessions restart ID`: their prompt keeps the old project) and, when DIR has saved sessions, that a running chi web groups them after its restart. This machine only. See [Projects without git](memory.md#projects-without-git)
|
|
40
|
-
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Plugins](plugins.md) and [Bundle hooks](hooks.md#bundle-hooks)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet
|
|
40
|
+
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Plugins](plugins.md) and [Bundle hooks](hooks.md#bundle-hooks)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet and a memory, plugin or script whose bytes disagree with its manifest `sha256`; without it such a bundle or file is refused (exit 1), nothing written. A plain `install` of a bundle that is already installed from the same source rewrites its rules, plugin, scripts and record: the summary lists a file that already holds the bundle's bytes as `Up to date: …`, one you wrote as `Skipped: …`, and ends with `NAME was already installed from SOURCE: its rules, plugin and scripts now match it and its record was rewritten` (plus, when a memory was kept, `your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)`). A source with class-file hooks (`hooks/*.rb` or a manifest `hooks:` entry) is refused, `--dry-run` too; an older install's hooks are removed by the next install or upgrade (`Removed (chi no longer loads class-file bundle hooks): …`, an edited one copied to the trash first; see [Class-file hooks are retired](hooks.md#class-file-hooks-are-retired)). `status NAME` lists the bundle's guardrail rules and scripts beside its plugin, each `[ok]`, `[missing]` or `[modified]` (edited: the next install replaces it, its copy kept in the trash; an edited rule doesn't load, so it counts in `issues=`), and `diff NAME` names an edited rule or script with its recorded and current sha256 (chi keeps no base text for them). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
|
|
41
41
|
|
|
42
42
|
- `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
|
|
43
43
|
- `chi guardrails rule <id>` — print exactly `asks` when that rule is loaded, not disabled and votes `ask` in this `guardrails.mode` for every model; any other line says why not (no such rule, disabled, another mode, it denies, for some models only, rules that failed to load, `guardrails.enabled: false`). Exit 0 either way, 2 for a usage error. It reads config.yml and the installed bundles as a worker does (`GuardrailWiring#rules`) and takes the rule's id, never a command; the [coordinator skill](bundles.md#the-coordinator-bundle) reads it before it merges without asking
|
|
@@ -512,9 +512,9 @@ invalid option (exit 1).
|
|
|
512
512
|
| `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
|
|
513
513
|
| `--set KEY=VALUE` | A setting for a new session (repeatable; a later one of the same key wins). A core key a session may set (`recap.enabled`, `recap.sentences`) is typed by the key and goes first, over the CLI, env and config.yml: `--set recap.enabled=false` turns the idle recap off for this session; a value it doesn't take (`recap.enabled=maybe`) or a key no session sets (`guardrails.mode`) is an error, exit 2 (see [Per-session settings](configuration.md#per-session-settings)). Any other name is a bundle's `BUNDLE.KEY`, laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
|
|
514
514
|
| `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
|
|
515
|
-
| `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline.
|
|
515
|
+
| `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Stored on the session; a fork and a continue keep the list too. A fork leaves out reserved `system/delegated` so it does not become a delegate. |
|
|
516
516
|
| `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
|
|
517
|
-
| `--mute NAME` | Hide a memory from this session (repeatable; a comma list too): its index line is not in the prompt, `memory_read` refuses it, the identity auto-load skips it, and it is dropped from the preloads (config baseline or `--memory`). A name matches in both scopes (`gh-helper`, `project/gh-helper` and `gh-helper.md` all hide `gh-helper`). Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
|
|
517
|
+
| `--mute NAME` | Hide a memory from this session (repeatable; a comma list too): its index line is not in the prompt, `memory_read` refuses it, the identity auto-load skips it, and it is dropped from the preloads (config baseline or `--memory`). A name matches in both scopes (`gh-helper`, `project/gh-helper` and `gh-helper.md` all hide `gh-helper`). A fork and a continue keep the list. Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
|
|
518
518
|
| `--llm-context LAYERS` | This session's own LLM context strategy: `none`, or layers such as `stale` or `stale,forget` (comma, space or `\|` separated). Saved in the session, before the model's `llm_context_strategy`. With `--resume` it is set on the resumed session; on an attached or resumed worker it runs as `/llm-context` before the first prompt (after `--model`'s `/model`). A value that isn't one is an error before anything starts. See "LLM context: a session's own strategy" in configuration.md. |
|
|
519
519
|
| `--llm-context-apply RULE` | This session's own apply rule: `payoff`, `next_request` or `turn_end`. |
|
|
520
520
|
| `--llm-context-budget TOKENS` | This session's own context budget: `64000`, `64k` (4k to 10M), or `off` (wins over the model's budget). |
|
|
@@ -804,7 +804,9 @@ file and index line stay as they are; only this session doesn't see it.
|
|
|
804
804
|
`muted_memory_names` in the session's JSON), written before the worker
|
|
805
805
|
starts. A worker respawned by `--resume`, `--attach` or `chi send` builds
|
|
806
806
|
the same prompt, and a `--resume ID --non-interactive` one-shot keeps the
|
|
807
|
-
lists too (merged with the flags it is given).
|
|
807
|
+
lists too (merged with the flags it is given). A fork and a continue keep
|
|
808
|
+
the lists too; a fork drops the reserved `system/delegated` memory, and a
|
|
809
|
+
continuation of a delegate child drops it because the new link is not a delegate.
|
|
808
810
|
- A mute wins: `--mute user_preferences` drops that config baseline entry for
|
|
809
811
|
one session, and `--memory x --mute x` is a mute (one warning line).
|
|
810
812
|
- Names are checked before the launch, warnings only: `Warning: --memory 'x'
|
data/docs/context.md
CHANGED
|
@@ -252,7 +252,16 @@ you're away from the terminal. All of these must hold:
|
|
|
252
252
|
- the session's worker is up and idle: no turn running, nothing queued, no
|
|
253
253
|
question or continue offer waiting, not just started. A changed push from a
|
|
254
254
|
session-scoped source can start an idle-exited worker; it never restarts an
|
|
255
|
-
explicitly stopped session
|
|
255
|
+
explicitly stopped session (the push waits for its next start). Only the
|
|
256
|
+
stop marker (`chi sessions stop`) counts as stopped. `chi context push`
|
|
257
|
+
ends its line with what that wake did: `· chi woken`, `· chi is stopped: it
|
|
258
|
+
sees this at its next start` or `· chi is starting` (nothing when the
|
|
259
|
+
worker is up, or the push asked no wake; chi web's push answer carries it
|
|
260
|
+
as `wake`). `· chi woken` means this push started the worker; one another
|
|
261
|
+
start had begun says `· chi is starting`. A worker from an older chi woken
|
|
262
|
+
for a session whose file still says stopped with no `chi sessions stop`
|
|
263
|
+
behind it exits at once: the line still says woken; restarting chi on the
|
|
264
|
+
new version covers it;
|
|
256
265
|
- under `session.max_wakes` turns in a row with no message from you (the
|
|
257
266
|
budget delegate reports and finished background tasks use too; a turn that
|
|
258
267
|
failed, or was stopped before the model call, pauses wakes until you write);
|
data/docs/guardrails.md
CHANGED
|
@@ -314,7 +314,9 @@ as before ([their review boundary](#a-participants-review-boundary)). The rule i
|
|
|
314
314
|
|
|
315
315
|
## Protected paths
|
|
316
316
|
|
|
317
|
-
Built in, for
|
|
317
|
+
Built in, for a call whose [targets](plugins.md#guardrails) say it writes
|
|
318
|
+
its paths: `write`, `edit`, `memory_write`, and a plugin tool's `targets:`
|
|
319
|
+
with `writes: true` (symlinks resolved; see
|
|
318
320
|
[How paths resolve](#how-paths-resolve)):
|
|
319
321
|
|
|
320
322
|
- deny: the approval store's dir, installed bundles (`memories/.bundles/`),
|
|
@@ -334,7 +336,9 @@ bundle: a child asks before it changes anything outside its folder, the work
|
|
|
334
336
|
tree top of the folder it was started in (a worktree from `delegate cwd:`,
|
|
335
337
|
else the parent's checkout). Rule `child-boundary`, source `core`; it asks when
|
|
336
338
|
|
|
337
|
-
-
|
|
339
|
+
- a call whose [targets](plugins.md#guardrails) say it writes its paths —
|
|
340
|
+
`write`, `edit`, and a plugin tool's `targets:` with `writes: true` —
|
|
341
|
+
names a path outside it (tmp dirs and memory files don't count);
|
|
338
342
|
- `execute` or `task_create` runs mutating git in another dir (`git -C`, `cd`,
|
|
339
343
|
`GIT_DIR`: what `git: outside_repo` matches), or a command that isn't
|
|
340
344
|
read-only starts outside it (its `cwd:`) or names a path in the repository's
|
|
@@ -344,7 +348,12 @@ else the parent's checkout). Rule `child-boundary`, source `core`; it asks when
|
|
|
344
348
|
even from its own worktree: a `git config` write (not `--get`, `--list`,
|
|
345
349
|
`--worktree`), `update-ref`, `worktree add|remove|move|prune`, `stash
|
|
346
350
|
drop|clear|pop`, creating or deleting a tag, and deleting, forcing or
|
|
347
|
-
overwriting a branch other than its own
|
|
351
|
+
overwriting a branch other than its own;
|
|
352
|
+
- any call whose `targets:` carry a `command:` (a plugin tool's) is checked
|
|
353
|
+
for git that changes what every checkout shares, and for a `cwd:` outside
|
|
354
|
+
the child's folder. Its command's text may not be shell, so the reads a
|
|
355
|
+
shell call gets (its `git -C`/`cd` dirs, its redirects, its read-only test)
|
|
356
|
+
are left out.
|
|
348
357
|
|
|
349
358
|
Read-only commands (`git -C ../app log`, `cat ../app/x`) and paths elsewhere
|
|
350
359
|
(`/usr/bin`, `~/.gem`) never ask, nor does a read-only command that writes only
|
data/docs/internals/bridge.md
CHANGED
|
@@ -195,8 +195,11 @@ ring holds the event. `serve!` orders a join so that nothing is missed or double
|
|
|
195
195
|
|
|
196
196
|
A **`reset`** frame (the same content, numbered with its own seq) is written when a cursor cannot be replayed: it is
|
|
197
197
|
from another epoch, ahead of the engine (a restarted worker), behind the ring's window (a cursor at `oldest - 1` has
|
|
198
|
-
seen everything before the window and replays), or the queue overflowed since the last drain. A cold connect
|
|
199
|
-
the
|
|
198
|
+
seen everything before the window and replays), or the queue overflowed since the last drain. A cold connect (no
|
|
199
|
+
cursor) is behind the window too once the ring has evicted an event (`RingBuffer#dropped?`: a replay would start mid-turn), so
|
|
200
|
+
it gets the `reset`; on a ring that never evicted (its first seq can be above 1: events before the Bridge started) it
|
|
201
|
+
replays the whole window. The clients re-sync on a `reset` and
|
|
202
|
+
reconnect with the snapshot's own id, which is inside the window (the web `resync`, the TUI's `follow`). Replay state is in memory per worker; there is no durable resume.
|
|
200
203
|
|
|
201
204
|
Each frame is `id: <seq>-<epoch>` (`EventId`: the seq first so a reader that takes it as a number still gets the
|
|
202
205
|
seq), `event: <type>`, `data: <json>`. A `: ping` goes out after `heartbeat_interval` (15 s) of silence. A watcher
|
|
@@ -293,9 +296,18 @@ The Bridge exposes:
|
|
|
293
296
|
doesn't: the client uses `generation_started`'s). Unknown provider fields never cross; a field is added here
|
|
294
297
|
when a client needs it, never as a catch-all bag. Details: [internals/context-telemetry.md](context-telemetry.md).
|
|
295
298
|
- `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
|
|
296
|
-
(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
+
(body: `{session_id, prompt, enqueued_id?}`; the optional id is a client-made UUID, generated by
|
|
300
|
+
the Bridge when absent or invalid). `BridgeClient#post_turn` supplies one when its caller does not,
|
|
301
|
+
including the attached TUI. The turn still goes through the worker's file-IPC input path — the
|
|
302
|
+
Bridge never calls `run_turn` across the HTTP boundary. Repeating an id already accepted by this
|
|
303
|
+
worker with the same prompt returns the original `202` acknowledgement, including its delivery and
|
|
304
|
+
cut outcome, and does not enqueue another turn; a different prompt with that id is refused with
|
|
305
|
+
`409 enqueued_id_conflict`. An input file with an already accepted id is consumed without running.
|
|
306
|
+
The worker keeps a bounded recent history of accepted ids.
|
|
307
|
+
For N-1 compatibility, an older worker ignores the optional field and generates its own id; an older
|
|
308
|
+
client sends none, so this Bridge generates one. Only the bridge's own session: another id, in the
|
|
309
|
+
path or the body's `session_id`, is `404 unknown_session`. Inspect results through the read surface,
|
|
310
|
+
not the turn response.
|
|
299
311
|
An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
|
|
300
312
|
a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
|
|
301
313
|
not run: a client that timed out has said the message was not sent. `/answer`,
|
|
@@ -307,7 +319,10 @@ The Bridge exposes:
|
|
|
307
319
|
timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
|
|
308
320
|
`501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
|
|
309
321
|
refused connection is `503 not_live`.
|
|
310
|
-
- `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
|
|
322
|
+
- `POST /session/:id/cancel` — cancel the running turn; `202`, or `409 not_running` when none runs. The optional
|
|
323
|
+
`turn_id` names the turn the Stop means (the web page and the TUI send the one they show): `TurnState#cancel!` compares
|
|
324
|
+
it to the running turn's id under its lock, and another turn running is `409 turn_mismatch`, nothing cancelled, so a
|
|
325
|
+
Stop pressed as a turn ends can't cancel the queued next one. No `turn_id` (an older client) cancels whichever runs.
|
|
311
326
|
- `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
|
|
312
327
|
min_user_turns}` (`{enabled: false, session_off}` without recaps; `session_off` when the session's own
|
|
313
328
|
`/recap off` turned it off); a new recap is asked for at once and
|
data/docs/internals/bundles.md
CHANGED
|
@@ -31,7 +31,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
31
31
|
|
|
32
32
|
| Piece | Role |
|
|
33
33
|
|---|---|
|
|
34
|
-
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it). `upgrade` resolves its argument by `upgrade_source`: a git URL or an existing path, then a shipped name, then the bare name of an installed bundle — whose record's source it then uses (a git URL with its `#ref`, a dir or an archive that is still there), printed as `Upgrading NAME from SOURCE`; a dev record is refused with `Installer.dev_install_line`, a recorded source that is relative, empty or gone says so and asks for the path or URL of a copy (`Installer.source_gone_line`, the words `ManifestStanding` uses too) |
|
|
34
|
+
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it). `upgrade` resolves its argument by `upgrade_source`: a git URL or an existing path, then a shipped name, then the bare name of an installed bundle — whose record's source it then uses (a git URL with its `#ref`, a dir or an archive that is still there), printed as `Upgrading NAME from SOURCE`; a dev record is refused with `Installer.dev_install_line`, a recorded source that is relative, empty or gone says so and asks for the path or URL of a copy (`Installer.source_gone_line`, the words `ManifestStanding` uses too). After an upgrade or uninstall that changed the bundle's plugin (or brought one in), one line names the sessions still running its old code (`LiveVersions.workers`, the ones whose worker takes a restart) and `chi sessions restart ID` for each: only the plugin stays in memory, since a bundle's rules reload by their stamp and its scripts run from the replaced files. |
|
|
35
35
|
| `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
|
|
36
36
|
| `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). Every dropped key is sent as nil, so the child really lacks it: a spawn env is merged onto the spawner's, so a key only deleted from the Hash is still inherited (`ChildEnv.spawn_env`). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
|
|
37
37
|
| `BundleCommand::ApiCalls` (bundle_command/api_calls.rb) | what a plugin's source calls of the API, by Prism: `#findings` (a call or keyword the API's classes lack, `check`'s `api`), `#keywords` (the floor check's `call(keyword:)`). Its only list is `MEMBERS`, which object a member gives (`ctx.sessions` → `Plugin::Sessions`); a new such object joins it |
|
|
@@ -131,8 +131,12 @@ has the bundle's own manifest at the recorded version (`Provenance#adopt_manifes
|
|
|
131
131
|
|
|
132
132
|
## Installer#run, in order
|
|
133
133
|
|
|
134
|
-
The order matters: warnings print in it
|
|
135
|
-
|
|
134
|
+
The order matters: warnings print in it. Before the first write, `AssetInstaller#check_named_files` raises `InstallError`
|
|
135
|
+
for a plugin or script the manifest names but the bundle lacks (or ships as a link): nothing is written then. With
|
|
136
|
+
`refuse_mismatch: true` (only `BundleCommand#installer_for` passes it), `Installer#refuse_memory_mismatches` and
|
|
137
|
+
`AssetInstaller#check_named_checksums` raise the same way for a memory, plugin or script whose bytes disagree with the
|
|
138
|
+
sha its manifest declares -- `--force` skips the check, a dev install follows its own tree, and a non-strict run has no
|
|
139
|
+
shas to trust; `chi update`, `SystemBundle` and a profile pass nothing and keep the post-write warning.
|
|
136
140
|
|
|
137
141
|
1. `load_source`: normalize (or take the `PreparedSource` `BundleCommand` passed as `prepared:`), read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning). Then `refuse_too_new`: a bundle whose `requires_chi` this chi doesn't meet raises `InstallError` unless `force` — nothing is written, dry run included (docs/plugins.md, "Loading, and when it fails").
|
|
138
142
|
2. `resolve_target`: scope = CLI > manifest > `system`; then `check_source` against the existing record
|
|
@@ -155,7 +159,8 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
155
159
|
rules. A file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`) is skipped with a
|
|
156
160
|
warning (a rule) or raises `InstallError` (the plugin, a script).
|
|
157
161
|
5. An upgrade's `prune_dropped_files`: each dropped file's `FileState` (`shipped: false`) and its `UPGRADE` / `FORCE` row: unchanged (`#edited?` false) → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
158
|
-
6. `verify_memory_checksums` (strict except a dev install, whose own tree may have stale shas;
|
|
162
|
+
6. `verify_memory_checksums` (strict except a dev install, whose own tree may have stale shas; the warning only, since a
|
|
163
|
+
run that refuses a mismatch (`refuse_mismatch: true`) never reaches it with one unless `--force`; `chi bundle check` flags them: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; a `Remerge` merge checks its incoming file too), `warn_missing_needs`.
|
|
159
164
|
7. `write_provenance` (manifest, not a dry run): each owned file's base comes from its action's `next_base`
|
|
160
165
|
(`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
|
|
161
166
|
file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
|
|
@@ -171,7 +176,21 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
171
176
|
A file system error in steps 3 to 5 (`Installer#replace_files`; a `.trash` it can't write, an edited file it can't read) is an
|
|
172
177
|
`InstallError` in `Trash.failure_message`'s words, as it is an `UninstallError` in `Uninstaller#run`. Each save comes before
|
|
173
178
|
its removal, so no edit is lost, and the record isn't written, so a retry is the same upgrade. Not atomic: files the
|
|
174
|
-
failed run already placed stay.
|
|
179
|
+
failed run already placed stay, and the intent journal says they are the bundle's.
|
|
180
|
+
|
|
181
|
+
**The intent journal.** After `check_named_files` and before the first write, a real run with a manifest writes
|
|
182
|
+
`.bundles/<name>/pending.json` (`Provenance#write_pending`, atomically like the record): `{"version": "2.0.0",
|
|
183
|
+
"scope": "project", "target_dir": "/path/to/project-memories", "files": {"a.md": ["<sha256>"]}}`: the target
|
|
184
|
+
`version` is the latest run's target version, while `files` holds the union of incoming shas recorded by interrupted
|
|
185
|
+
runs for that scope/target directory. A retry of another version in the same target adds its incoming shas; a journal
|
|
186
|
+
for another scope directory is ignored. `Provenance#write` removes it with the record write; an uninstall's `rm_rf`
|
|
187
|
+
sweeps it. `FileState.of` reads it:
|
|
188
|
+
a file whose disk sha is one of its shas is that bundle's text at the pending version, owned and its own base (an entry
|
|
189
|
+
made up when the record has none), so a killed fresh install's copy isn't `:user_same` (kept as the user's for good) and
|
|
190
|
+
a killed upgrade's fast-forward isn't `:edited` (a conflict at the next version). Install, upgrade, uninstall and
|
|
191
|
+
`chi bundle status` all go through it. A file matching none of its shas follows the usual rules; a merged file isn't
|
|
192
|
+
journaled (its bytes are no bundle text). Not covered: a journal left by a run that failed before writing a file whose
|
|
193
|
+
text the user already had is read the same way.
|
|
175
194
|
|
|
176
195
|
Which files a plan asks `Merger::Upgrade` about is the caller's, and the lists differ, each less the files the bundle
|
|
177
196
|
doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' every shipped `*.md`, so `chi update`'s plan
|
|
@@ -61,7 +61,10 @@ the payload, so an edit and a continue carry none: a new link starts with no las
|
|
|
61
61
|
## Editing
|
|
62
62
|
|
|
63
63
|
`/objectives` (`ObjectivesCommand`, registered `anytime`) reads `Objectives.current`. An edit goes out through
|
|
64
|
-
`NoteDelivery.deliver(objectives:)`, which means:
|
|
64
|
+
`NoteDelivery.deliver(objectives:, pin: true)`, which means:
|
|
65
|
+
|
|
66
|
+
- It stays in its own session, even one that was continued: the list is built from this conversation, so it never
|
|
67
|
+
follows the chain to the newest link (as other notes do) and overwrites that link's list.
|
|
65
68
|
|
|
66
69
|
- One edit's read and write are one step (`Objectives.with_lock`, a file lock in the session's folder, across threads
|
|
67
70
|
and processes), and the worker's absorb holds it from its claim to the files' delete, so no edit reads between them.
|
|
@@ -176,7 +179,10 @@ command), which absorbs the file. It no longer waits for the fallback tick.
|
|
|
176
179
|
`SessionChain.note_text` (capped; the last answer gets what the recap leaves). It reads the previous link fresh,
|
|
177
180
|
with its unabsorbed newest file. A failure is only logged (`continue_objectives_failed`), because it runs inside the
|
|
178
181
|
continue's rollback. A review link's list stays behind, as its last answer does: the next link's participants could
|
|
179
|
-
differ.
|
|
182
|
+
differ. The previous link's other waiting notes move after it (`NoteMove.move`, the step after: moved first, the
|
|
183
|
+
carry would miss a pending edit, and the stale list would be the batch's newest); its objectives notes never move,
|
|
184
|
+
and the new link's worker never adopts one from it (`NoteMove.adoption`), so a list from the archived link can't
|
|
185
|
+
overwrite the carried one.
|
|
180
186
|
- **Rollback**: a note survives `TurnFlow#restore`, and an unabsorbed edit is on disk, untouched.
|
|
181
187
|
- **Fork, delegate**: a plugin fork copies what it is passed. A delegate starts with an empty fold, so the tool answers
|
|
182
188
|
that there are none.
|
|
@@ -134,7 +134,8 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
|
|
|
134
134
|
`parent_continue_refused`, `no_vision`, exit 1) or `BadRequest` (the other `invalid_*`, `missing_fields`, `bad_images`,
|
|
135
135
|
exit 2, the usage-error path). That is for the routes `raise_refusal` reads (create, uploads, …). `POST …/turn` is
|
|
136
136
|
mapped in `Remote#deliver_turn`: any 4xx but 404 and `owned_by_tui` becomes a refused `DeliverResult` with the
|
|
137
|
-
server's `ack`, which `SendCommand` prints as `<id> refused
|
|
137
|
+
server's `ack`, which `SendCommand` prints as `<id> <refused|failed>: <detail>` (`DeliverResult#refusal_text`,
|
|
138
|
+
whose refused codes are `DeliverResult::REMOTE_REFUSALS`), exit 1.
|
|
138
139
|
A per-method mapping covers the rest (`Remote#deliver_turn`, `#note`, `#context_push`, `#context_remove`,
|
|
139
140
|
`#bridge_reply` for answer/dismiss: chi web's 503 `not_live` or 404 `not_found` → nil, i.e. no worker; 504 → the
|
|
140
141
|
Bridge's 408; 501 → its 404). `#context_push` maps 409 to `:not_push` and 410 `context_continued` (a continued
|
data/docs/memory.md
CHANGED
|
@@ -44,8 +44,10 @@ the folder is left as is: it is created when missing, nothing is marked).
|
|
|
44
44
|
by its path (memories, project context sources, the skills bundle's kept versions) move to the id,
|
|
45
45
|
each leaving a link by its old name (an older chi, keying by path, finds the same folder), then the
|
|
46
46
|
id is written. It refuses while a session of the project runs (stop it first, so nothing writes to
|
|
47
|
-
the old folder mid-move), and names memories an earlier
|
|
48
|
-
another hash:
|
|
47
|
+
the old folder mid-move), and names memories an earlier key left (the same folder name with
|
|
48
|
+
another path's hash: moved before it had an id, or a `chi project init` id folder,
|
|
49
|
+
`<basename>-<8 hex>`, left behind when `git init` turns that marked folder into a repository),
|
|
50
|
+
merging nothing. A link stays behind: a new
|
|
49
51
|
folder (or git repository) made later at the old path reaches the moved project's memories
|
|
50
52
|
through it; delete the link to stop that. chi web's project picker marks a new folder with an id
|
|
51
53
|
but never moves folders: a folder it marks that already has path-keyed memories gets no id until
|
data/docs/plugins.md
CHANGED
|
@@ -264,7 +264,7 @@ run by `call_tool` as the tool itself ([the chi group](#chitool_sourcecall-acts_
|
|
|
264
264
|
it raises, the default is shown. Both are saved with the call's result, so
|
|
265
265
|
a web page reloaded later shows the same row: the web server doesn't run
|
|
266
266
|
plugins.
|
|
267
|
-
- `targets`: `->(args) { { paths: [...], command: "…", cwd: "…", acts_as: "…", args: {…}, label: "…" } }`,
|
|
267
|
+
- `targets`: `->(args) { { paths: [...], writes: true, command: "…", cwd: "…", acts_as: "…", args: {…}, label: "…" } }`,
|
|
268
268
|
each key optional, says what a call acts on, for [guardrails](#guardrails).
|
|
269
269
|
- `refs`: `->(args) { [{ kind:, target:, label:, actions: [] }, …] }`, what the call's row traces to in the web
|
|
270
270
|
(display only, the model never sees them). See [Refs on a tool's row](#refs-on-a-tools-row).
|
|
@@ -335,8 +335,15 @@ is what `targets:` says:
|
|
|
335
335
|
- `paths:`: files the call reads or writes, absolute or relative to `cwd:`
|
|
336
336
|
(else the session's directory). `path:` globs, `outside_repo` (measured
|
|
337
337
|
from the session's repo, whatever the call's `cwd:`) and the protected
|
|
338
|
-
paths (chi's config, …) match them.
|
|
339
|
-
|
|
338
|
+
paths (chi's config, …) match them. A call that writes them adds
|
|
339
|
+
`writes: true` (what `write` and `edit` carry): the protected paths and a
|
|
340
|
+
delegate child's boundary then judge its paths as they do a write's. Say
|
|
341
|
+
it when the call writes, never when it only reads (a read's paths are
|
|
342
|
+
`path:` rule material, not a write); a tool without it is judged as
|
|
343
|
+
today.
|
|
344
|
+
- `command:`: a shell command the call runs; `command:` rules match it, and
|
|
345
|
+
a delegate child's boundary checks where it runs and whether it changes
|
|
346
|
+
what every checkout shares (as for `execute`).
|
|
340
347
|
- `cwd:`: where it runs, for the repo root and relative paths.
|
|
341
348
|
- `acts_as:`: the tool a call stands for, when the tool runs other tools
|
|
342
349
|
(chi's `call_tool`, which calls a tool a [source](#chitool_sourcecall-acts_as-label-wait-prefix) published). Rules keyed by that tool's name match the
|
|
@@ -1689,7 +1696,9 @@ gem.
|
|
|
1689
1696
|
|
|
1690
1697
|
- `chi bundle install <dir>`: copies the plugin to
|
|
1691
1698
|
`<memories>/.bundles/<name>/plugin/`. It records the plugin's sha256 and
|
|
1692
|
-
`requires_chi`, and
|
|
1699
|
+
`requires_chi`, and refuses a plugin or script whose bytes disagree with its
|
|
1700
|
+
declared sha256 (`--force` installs it anyway, with the `Checksum mismatch`
|
|
1701
|
+
warning; an automatic `chi update` of a shipped bundle only warns). A bundle whose
|
|
1693
1702
|
`requires_chi` this chi doesn't meet is **refused**: `Install failed: NAME
|
|
1694
1703
|
can't be installed: it requires chi >= X (this is chi Y)`, exit 1, nothing
|
|
1695
1704
|
written. `--force` installs it anyway, with the warnings that its plugin,
|
data/docs/releasing.md
CHANGED
|
@@ -76,7 +76,7 @@ as the GitHub release body.
|
|
|
76
76
|
| `rake lint` / `rake lint:fix` | RuboCop over the repo (must report no offenses; CI runs it) / its safe autocorrect (`rubocop -a`). |
|
|
77
77
|
| `rake gem:validate`, `gem:build`, `gem:install`, `gem:clean` | Check the gemspec; build the gem into `pkg/`; build and install it locally; remove the built gems. |
|
|
78
78
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
79
|
-
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line, and each entry an `# Unreleased` comment in `Plugin::Api::SINCE` or `Manifest::SINCE` names moved into the table at X.Y.Z (the comment dropped). No commit. |
|
|
79
|
+
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, a compatibility corpus at `spec/compat/X.Y.Z/` written through the current state writers, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line, and each entry an `# Unreleased` comment in `Plugin::Api::SINCE` or `Manifest::SINCE` names moved into the table at X.Y.Z (the comment dropped). No commit; commit the corpus with the release. |
|
|
80
80
|
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, no `# Unreleased` comment left in `Plugin::Api::SINCE` or `Manifest::SINCE`, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
81
81
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
82
82
|
|
|
@@ -97,9 +97,12 @@ The agent does each step and stops where the user has to say yes.
|
|
|
97
97
|
markers are gone) and the profiles it raised with them (a patch bump of
|
|
98
98
|
each one's version, none for a profile already bumped since the last tag),
|
|
99
99
|
which it names in the CHANGELOG's "Update with `chi update`" line, and the
|
|
100
|
-
SINCE entries it moved out of their `# Unreleased` comments.
|
|
101
|
-
|
|
102
|
-
`
|
|
100
|
+
SINCE entries it moved out of their `# Unreleased` comments. It also records
|
|
101
|
+
the release's session, bridge, cards, schedules and bundle state under
|
|
102
|
+
`spec/compat/X.Y.Z/`; commit those samples with the release so the next
|
|
103
|
+
release's reader spec checks them as N-1 data.
|
|
104
|
+
5. **Check.** Add all release files, including the new compatibility corpus, then run it:
|
|
105
|
+
`git add -A && git commit -m "Release X.Y.Z"` and `bundle exec rake release:check`.
|
|
103
106
|
Fix anything it finds in new commits and run it again.
|
|
104
107
|
6. **Push main.** `git push origin main`; wait for CI to go green.
|
|
105
108
|
7. **Tag and push the tag.**
|
data/docs/scheduling.md
CHANGED
|
@@ -7,6 +7,7 @@ worker's memory, so it outlives every session, every restart and every crash:
|
|
|
7
7
|
chi web's hub can never lose an update to each other). A **tick** — cron, systemd,
|
|
8
8
|
launchd, `chi schedule tick`, or chi web's hub when `schedule.auto_tick` is on —
|
|
9
9
|
walks the entries whose `next_due` has passed and runs the ones that are not busy.
|
|
10
|
+
The top-level `last_tick_at` records the last acquired, non-dry-run tick.
|
|
10
11
|
|
|
11
12
|
The file is versioned: each entry member is required or declares the format
|
|
12
13
|
version and default that introduced it. Rewrites preserve unknown keys and
|
|
@@ -104,7 +105,10 @@ A cron tick cannot read a worker's memory, so the tick has two paths.
|
|
|
104
105
|
|
|
105
106
|
**No live worker** (the usual case for a weekly schedule): the files say what can
|
|
106
107
|
be known — the saved status is not `running` (and not inside the starting grace),
|
|
107
|
-
`input/` is empty, no step-limit question is open
|
|
108
|
+
`input/` is empty, no step-limit question is open, and, for chain mode, no worker
|
|
109
|
+
is being started for it (the `<id>/starting` marker every spawn writes: another
|
|
110
|
+
ticker's firing that just continued the chain; its archive refuses it too, a
|
|
111
|
+
skip). Nothing else can be busy.
|
|
108
112
|
|
|
109
113
|
**A live worker**: ask it, over the new Bridge route
|
|
110
114
|
`GET /session/:id/activity` (`Worker#activity_snapshot`, the same signals
|
|
@@ -162,6 +166,16 @@ both counters**, so twelve skips spread over months do not kill a healthy weekly
|
|
|
162
166
|
schedule. `list` prints the skip budget as a time window — 12 skips at a 5 minute
|
|
163
167
|
tick is about an hour of patience, which is what the count really measures.
|
|
164
168
|
|
|
169
|
+
An accepted delivery is **pending**, not fired: `schedules.json` records its
|
|
170
|
+
session and turn id, and the next tick follows that turn's own outcome, even if a
|
|
171
|
+
later turn has replaced `last_turn`. A completed turn is a success; a failed or
|
|
172
|
+
canceled turn counts toward `max_failures`, with its reason in `chi schedule list`
|
|
173
|
+
and `show`. A question does not time out with the 40-second firing step: the turn
|
|
174
|
+
stays pending until its hard limit, derived from the schedule interval (at least
|
|
175
|
+
10 minutes and at most 6 hours). If no outcome is known by then, it counts as a
|
|
176
|
+
failure with reason `no outcome`. Failed, canceled and expired turns are not
|
|
177
|
+
re-fired; `next_due` remains at the next occurrence.
|
|
178
|
+
|
|
165
179
|
A chain holds at most `schedule.max_per_session` enabled schedules (default 10) and
|
|
166
180
|
the machine `schedule.max_total` (default 50); `schedule_task` and `chi schedule add`
|
|
167
181
|
refuse past them, naming the key. A schedule the chain already has — the same prompt
|
|
@@ -175,7 +189,13 @@ is refused with exit 2, naming the matches.
|
|
|
175
189
|
The tick's lock covers the file only, never the action: the clock advances before
|
|
176
190
|
the work starts — for `every` and `weekday` to the next occurrence, for a `date`
|
|
177
191
|
one-off to none — so a tick killed mid-continue cannot fire the same entry twice.
|
|
178
|
-
|
|
192
|
+
If a later tick finds a firing older than 40 seconds without an accepted turn, it
|
|
193
|
+
counts an `interrupted` failure. This 40-second limit covers only the firing
|
|
194
|
+
step, not an accepted turn or its pending question. The action may already have
|
|
195
|
+
taken effect before the process died, so that occurrence is not replayed; a
|
|
196
|
+
`date` one-off stays disabled with the failure visible, and recurring schedules
|
|
197
|
+
keep their advanced clock. An accepted turn is tracked separately until its own
|
|
198
|
+
outcome is known or its hard limit is reached.
|
|
179
199
|
And a `chi schedule add` from a terminal never waits on an archive or a recap wait. Chain mode targets `last:<head>`, so `SessionContinue.run`'s
|
|
180
200
|
own retry loop follows a continue a human made between our resolve and its lock
|
|
181
201
|
instead of racing it.
|
|
@@ -204,6 +224,11 @@ pruned" instead of linking to a deleted session.
|
|
|
204
224
|
If a link to prune still has a worker (`DeleteRefused(:worker_running)`), pruning
|
|
205
225
|
stops there for this firing and is retried at the next one.
|
|
206
226
|
|
|
227
|
+
Retention keeps the stored `anchor` and `head`, as well as the resolved target,
|
|
228
|
+
of every enabled schedule. Deleting a protected session is refused—even with
|
|
229
|
+
`chi sessions delete --force`—until the schedule is cancelled with
|
|
230
|
+
`chi schedule cancel ID`; a disabled schedule does not protect its sessions.
|
|
231
|
+
|
|
207
232
|
## Running the tick
|
|
208
233
|
|
|
209
234
|
`chi schedule tick` is quiet and safe to run often; it is this machine only
|
|
@@ -220,7 +245,9 @@ Run it from systemd or launchd instead if you prefer a timer: the command is the
|
|
|
220
245
|
same, `chi schedule tick`, and the lock makes overlapping runs harmless — the
|
|
221
246
|
second one exits 0 with "no schedules due (another tick holds the lock)".
|
|
222
247
|
`--dry-run` prints what would fire and changes nothing; `--format json` is the seam
|
|
223
|
-
for scripts and for a future web UI.
|
|
248
|
+
for scripts and for a future web UI. `chi schedule run-now` reserves the entry
|
|
249
|
+
under the same lock before starting its action, so it cannot race a tick into a
|
|
250
|
+
duplicate firing.
|
|
224
251
|
|
|
225
252
|
A tick fires **one entry per chain**: of the due entries of one chain, the one due
|
|
226
253
|
earliest (`next_due`, then id) fires and the rest are skipped — "the chain just
|
|
@@ -234,8 +261,13 @@ they queue a turn, they do not archive.
|
|
|
234
261
|
|
|
235
262
|
`schedule.auto_tick: true` makes chi web's hub tick on
|
|
236
263
|
`schedule.tick_interval_minutes` (default 5) as well, sharing the same lock, so
|
|
237
|
-
cron and the hub can coexist without firing twice.
|
|
238
|
-
|
|
264
|
+
cron and the hub can coexist without firing twice. The hub runs its tick outside
|
|
265
|
+
the session scan loop and allows only one hub tick at a time, so a firing waiting
|
|
266
|
+
on a worker does not stall session updates. It is off by default: cron or systemd
|
|
267
|
+
is the documented driver. When it is off and no tick has run within twice
|
|
268
|
+
the configured interval, `chi schedule add`, `chi schedule list` and `schedule_task`
|
|
269
|
+
warn to turn `schedule.auto_tick` on or run `chi schedule tick` from cron; `--dry-run`
|
|
270
|
+
and lock-held attempts do not refresh `last_tick_at`.
|
|
239
271
|
|
|
240
272
|
See [cli.md](cli.md) for the subcommands and flags, and
|
|
241
273
|
[configuration.md](configuration.md) for the `schedule.*` settings.
|