Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/black-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,6 @@ test/test_chat_nav.py
test/test_chat_slot_create_folder.py
test/test_chat_slot_end_wait.py
test/test_chat_slot_interrupt.py
test/test_chat_slot_reasoning_effort.py
test/test_chat_title.py
test/test_cli_desktop.py
test/test_cli_lazy_imports.py
Expand Down
32 changes: 32 additions & 0 deletions docs/reference/kiro-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,38 @@ a supported range, and a 3.x CLI satisfies all of them: `mcp_hot_reload.py`'s
`kiro_cli.py`'s `SPEC_PERMISSIONS_MIN_VERSION` (2.23.0, the agent-spec
`permissions` block) and `MANDATORY_MCPS_DROP_MIN_VERSION` (2.27.0, deferring
Crew's MCP servers under Tool Search).
None of these floors is effort-related: the `kirocrew.effortOwned` ownership
record in the workspace `cli.json` counts only while its sibling
`kirocrew.effortOwnedStamp` equals the file's whole-second mtime, so it depends
on kiro-cli tolerating two keys it does not know when it loads workspace
settings, verified on kiro-cli 2.27.1. The one tripwire is
`test/test_kiro_cli_effort_ownership_pin.py`, which fails when the BUNDLED pin in
`packaging/kiro-cli-version` is raised past that verified version; a `kiro-cli`
the host installed itself is not covered by it. After a kiro-cli bump, re-verify two things
on the new version. First, kiro-cli still loads a workspace `cli.json` carrying
the `kirocrew.effortOwned` and `kirocrew.effortOwnedStamp` keys (`settings list`
shows the workspace `chat.modelDefaults`, and a chat spawns and answers), and
after a `kiro-cli settings --workspace` write the file's whole-second mtime no
longer equals the recorded stamp, so Kiro Crew's readers treat the record as void
instead of claiming the rewritten entries. Whether that write keeps or drops the
two keys does not matter; a write that left the stamp equal to the new mtime
would. Second, a live `/effort <level>` push still sets the
session's effort: kiro-cli's next `_kiro.dev/metadata` notification reports that
level, under `reasoning.effort` on kiro-cli 2.27.1. That push is how a chat keeps
its own level over a shared overlay that holds another one, and kiro-cli 2.27.1's
ACP command list marks `/effort` deprecated. On kiro-cli 2.34.0 the next
`_kiro.dev/metadata` after `/effort high` and after `/effort medium` reported that
level, and neither those pushes, a `session/set_model` away and back, nor the
session's start and close changed a byte or an mtime under the work directory's
`.kiro` or in the user-level `cli.json`. kiro-cli 2.34.0 answers a command name
it does not know with a JSON-RPC error (`-32700`), which makes the post-spawn push
raise and `_reassert_projected_effort` log a warning, so a build that removes
`/effort` is not silent. It answers a level it refuses with `success: false` and
no error, which no `/effort` sender reports yet (#23981). If the push does not
land, move every
`/effort` sender to kiro-cli's replacement channel before the bump ships. Then raise
`EFFORT_OWNERSHIP_VERIFIED_KIRO_CLI` in that test and update the
verified-version note in `docs/system-specs/modules/providers.md`.
So these pages describe what upstream published on the fetch date recorded per
row, not a supported-version contract.

Expand Down
12 changes: 11 additions & 1 deletion docs/system-specs/modules/acp-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,16 @@ repository's exclude list. Only missing lines are appended, the
user's own lines are never rewritten, and a failed exclude write never blocks
the overlay. A repository that already tracks `.kiro/settings/cli.json` still
sees it modified: git's exclude list does not hide tracked files (known limit).
The lock admits a `.kiro` or `settings` link component by component. It uses
`lstat` and `readlink` only on names already inside the resolved work directory,
then decides containment from the link text before probing the target. A link
naming a remote, outside, other-drive, or looping path is refused without being
opened or resolved. Projection and rollback then read and write the admitted
folder. A clear or Default under a refused link reports that it could not change
the file, whether or not the target contains `cli.json`. A component swapped for
a link after admission is still refused by the pinned walk (see
[providers](providers.md)); an outside link skips projection and logs the existing
refusal.
Crew records the original local inheritance key's presence and value in
`kirocrew.skillDiscovery.previousInheritance`. Rollback restores that snapshot
only while the native key still equals Crew's asserted `true`, removes Crew's
Expand Down Expand Up @@ -474,7 +484,7 @@ removes it, and its name is no sweep candidate. The doctor's census below still
counts it with the unmarked folders, since counting deletes nothing. The key
predicate and corresponding directory-name regex come from one shared shape table;
stable `cron:<job>:<agent>` keys are not one-run directories.
`AcpProvider.start` writes a `.kirocrew-run-dir` marker before
`AcpProvider.start` (`providers/acp_lifecycle.py`) writes a `.kirocrew-run-dir` marker before
any other writer: two ASCII lines, the data-home identity (`data_home_id()`, the
digest of the resolved `config_dir()`) and the gateway pid. That marker is the
directory's only provenance: a name under the workspace root is not one (a person
Expand Down
18 changes: 12 additions & 6 deletions docs/system-specs/modules/providers.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/system-specs/modules/session-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ unreachable in production because the caller's `X-Internal-Secret` is ignored.
| `session_end_wait` | `POST /api/session-control/end-wait` | Wake a session the caller CREATED from the `wait` tool early, keeping its turn; any other target is refused `not_creator`, for every caller class |
| `session_retry` | `POST /api/session-control/retry` | Re-run a failed last turn through `continue_slot_turn`, the Resume button's path, with `require_interrupted=True` (`turn_not_failed` otherwise) and `authorize_target` re-run synchronously under the slot lock before the continuation is queued |
| `session_continue` | `POST /api/session-control/continue` | The same `continue_slot_turn` call without `require_interrupted`, as the Continue button makes it, through the helper `retry_target` shares (`_continue_turn_of_target`); the result's `body` is `resumed` or `continued`, whichever continuation `_is_interrupted` chose, and the allowed audit line records it. Not in any auto-grant: a member or conductor is asked before each call |
| `session_set_model` | `POST /api/session-control/set-model` | Record a pending model and/or reasoning-effort pick on an idle session; `apply_pending_model_pick` commits it at the start of the target's next turn after re-running `authorize_target` in the same synchronous step. Each half yields separately to a newer user pick from its own control. A busy target is refused with `target_busy` and keeps its settings |
| `session_set_model` | `POST /api/session-control/set-model` | Record a pending model and/or reasoning-effort pick on an idle session; `apply_pending_model_pick` commits it at the start of the target's next turn after re-running `authorize_target` in the same synchronous step. A committed effort level clears the key's pending explicit Default in that step. A later effort-picker choice on any slot driving that session that changes its explicit-Default flag (a Default pick, or a level pick that clears a pending Default) bumps every open slot driving the session and supersedes the pending effort half; a level pick with no Default pending does not. If saving the clear fails or is cancelled, the pick, slot values, and flag are restored so the current turn keeps its prior values and the next turn retries the pick. Each half otherwise yields separately to a newer user pick from its own control. A busy target is refused with `target_busy` and keeps its settings |
| `session_reload` | `POST /api/session-control/reload` | Relaunch the agent process of an idle session the caller created, through `chat_handlers.reload_slot_session` (shared with the tab menu's Reload session). The transcript is kept and gets one notice naming the caller. Self, remote-crew and busy targets (turn running or starting, queued messages, sub-agents) are refused |
| `session_rename` | `POST /api/session-control/rename` | Set the caller's own title or a target's through `chat_title.apply_manual_title`, the sidebar rename's code, after `authorize_target` with `allow_self`; a scheduled run's own session is still refused as `unattended_target`. The title is final (`_TITLE_ORIGIN_USER`). An empty title, one over 200 characters (before or after `_scrub_text` redaction, which lengthens credential-shaped text into markers), one with no character that renders (only whitespace, characters `terminal_safe._is_invisible` reports such as zero-width, variation-selector and Hangul-filler characters, or the blank Braille pattern U+2800), one containing U+2800 anywhere (scanning normalization keeps it, so it could split a secret past the redactors), or one with a control character or any text-direction control (embeddings, overrides, isolates and the U+200E/U+200F/U+061C marks), or any other `Cf` format character except U+200C/U+200D and the tag characters U+E0020..U+E007F that spell flag subdivisions (`_is_allowed_format_char`; the tag block's other `Cf` character, U+E0001 LANGUAGE TAG, is refused; its unassigned code points are not `Cf`), is refused with `invalid_title`; if the history write fails the live slot keeps and broadcasts the new title (best-effort, like the sidebar and channel renames; the next full save writes it) and the verb answers `title_persist_failed` (500) instead of `ok` |
| `session_generate_title` | `POST /api/session-control/generate-title` | Regenerate a session's title through `chat_title.generate_manual_title` and `chat_title.apply_regenerated_title`, the sidebar "Regenerate title" code: one awaited titling-model call; the result replaces the current title (a hand-set one included) and stays auto. After `authorize_target` (no `allow_self`), the target must be one the caller created (`not_creator`, 403) for every caller class. One admitted call per target per `create_rate_limit.WINDOW_SECS` (300 s) through the shared creation limiter keyed by the target slot, stamped before the model call (`title_rate_limited`, 429). After the model answers, the slot identity and closing flag are checked (`target_replaced`, 409) and `authorize_target` runs again synchronously; then every field is assigned before the first await, as `session_rename` does, and the save only makes that change durable. The save passes `still_current` (slot identity), so `_persist_title` refuses the write under the history lock when the slot left its key, and the verb then answers `target_replaced` (409) rather than overwriting a successor's saved title. A rename that landed during the model call wins (`changed: false`). If the history write fails the live slot keeps the title and the verb answers `title_persist_failed` (500). The sidebar push is skipped when the slot left its key during the save |
Expand Down
56 changes: 55 additions & 1 deletion docs/system-specs/modules/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -3242,7 +3242,61 @@ refused at the same gate, tightening a row already retained is never refused,
and no retained row is ever evicted -- it is the record the channel gate
hydrates from. Retiring rows once the header carries the mode needs the channel
gate to read the header, a separate change. Immortality of durable
settings stays opt-in (`_DURABLE_FLAGS`).
settings stays opt-in (`_DURABLE_FLAGS`). `EXPLICIT_EFFORT_DEFAULT_FLAG` is one
of them: an explicit Default that could not remove the model's workspace
`cli.json` effort entry itself leaves it on the key, and the first cold start of
that key that applies it clears it. It is carried there, not applied at the
pick, because the cold start resolves the work directory and the model whose
entry it removes, and either can change after the pick. A start that applies
the flag clears it. A level `session_set_model` committed at the turn start
clears a pending flag in the same step, and the turn saves that clear before it
acquires a session. A later effort-picker choice on any slot driving the same
session that changes the session's explicit-Default flag (a Default pick, or a
level pick that clears a pending Default) bumps every open slot driving that
session and supersedes the call's effort half. A level pick with no Default
pending does not. A
failed or cancelled save puts the pick, the slot's values and the flag back; the
turn runs on the values it had, and the next turn applies the pick again. The
flag stays armed on disk until that start has applied the Default and the clear
is saved. A pinned start rewrites the model's entry before its spawn; an
unpinned start rewrites the served model's entry after its spawn, once the
backend names that model; a harness that never reads `cli.json` applies the
Default without changing the file (details in providers.md). A kill before
the saved clear therefore leaves the next cold start armed, so it applies the
Default again. That replay finds the entry already gone, unless a level was
written by hand after the killed start; that level is replaced just as one
written between the original pick and its first start is replaced. A start
carrying a `reasoning_effort_override` runs that level, arms nothing and leaves
the flag pending for the key's next start without an override: the override is
not a newer effort action on the key (a level pick through the effort handler,
or a level `session_set_model` commits at the turn start, clears the flag itself)
but a slot's level read before the Default pick landed, an alias slot's own
level (two slot objects can drive one session), or a caller's pin.
Explicit-Default rows are bounded, and a pick past that bound is refused
before it changes the slot. Clearing the flag on a row that holds nothing else
removes the row, so none is left outside that bound. A pick that clears a key's flag holds its row until it ends
(`SessionMap.hold_explicit_effort_default`), so putting the flag back after a
failed save or a rollback needs no new row and cannot be refused.

Dashboard eager prefetch takes the session-keyed switch lock from its slot effort
read until `get_or_create` holds the key's allocation reservation, handing it off
at the cold-start gate's entry (`on_gate_queued`, the first hook past the
reservation). A concurrent effort picker therefore either commits before the
prefetch re-checks its captured bindings, causing that prefetch to stand down, or
waits until the reservation is held, where a pick that would change the effort
basis is refused (`effort_basis_locked`) until the allocation has registered.
Same-key starts that both read a pending explicit Default are serialized: the
first reserves it before the pool decision, and the others wait, without an
allocation reservation, until it registers or fails, then retry. A pick can land
between the owner's release and a waiter's retry; the retry re-reads the effort
basis, so it acts on that newest pick. It cannot record `explicit_effort_default` between a stale level read and the
allocation reservation. The lock is not held across the gate wait itself: a
person's first turn takes this lock before it allocates, and a prefetch queued
behind the BACKGROUND cold-start cap would otherwise make that turn wait out the
prefetch's whole start instead of running its own FOREGROUND start beside it.
The prefetch passes `wait_if_busy=False` while holding this lock: if a real turn
registered first, `SessionBusyError` makes speculation stand down rather than wait
for a lease under a lock the live turn's refusal fallback may need.

**Mapped-session enumeration:** `SessionMap.mapped_sids_by_key()` returns session
key → kiro-cli session ID for every entry that has one. Disk accounting
Expand Down
Loading
Loading