A provider can accept a request, return response headers, and then never
send a byte of body data. The connection-phase request timeout was cleared
as soon as headers arrived, so nothing bounded that wait and the agent turn
hung indefinitely after a tool call completed: step-finish:tool-calls was
recorded and the next step-start never arrived, with the HTTP server still
responsive.
Extend the same configured timeout deadline to the wait for the response
body's first byte. The connection-phase timer covers the fetch up to
headers; once headers arrive, the remaining deadline is handed to a
first-byte guard that aborts the request if no data arrives. After the
first byte the guard becomes a passthrough, so idle gaps inside an already
streaming response (reasoning, buffering, slow token generation) are never
touched and remain opt-in via chunkTimeout.
This is a transport-level signal (bytes on the wire, before any content)
rather than the absence of normalized AI SDK events, so it cannot fire on
long prompt processing or reasoning the way the reverted stream watchdog
did. timeout: false still disables the bound entirely.
Adds a hermetic regression test that injects a simulated stalled socket
through the provider's own fetch option via the plugin config hook, so the
SDK, Kilo's fetch wrapper, SSE parsing, the processor and the agent loop
all stay production code. The stalled request is transient, so the test
asserts the turn recovers by retrying and completing instead of freezing.
The test goes red without the fix (no retry, frozen at step-finish) and
green with it.
Refs #8656
CI:
- Render the config console's active overlay target from its new object
shape instead of passing it to JSX.
- Make the config overlay `expected` revision optional in the schema,
writer, and handler so clients without a binding write unconditionally
instead of receiving a 400, and add the missing PUT /indexing/consent
exerciser scenario. Regenerate the SDK for the schema change.
- The multi-project Storybook story called useLanguage() outside its
provider and rendered nothing; it now uses the story translator.
- Scope indexing test select locators by row title, since the tab gained a
project selector that shifted positional lookups.
Review findings:
- Gate worktree creation, promotion, and multi-version creation on the
target project's state: waitForStateReady only tracked the active
project, so those handlers could mutate a background project's state
before it loaded.
- Re-check trust and enablement for every project-stamped message instead
of resolving contexts through the unchecked map lookup.
- Register projects through resolveProjectRoot so a folder inside a linked
worktree cannot duplicate an existing project, and fix that helper to
issue valid rev-parse commands.
- Validate the persisted activeTarget shape before applying it.
- Stop throwing from workspace/session directory resolution: it runs
eagerly per webview message, where a throw dropped the message.
- Evict superseded config bindings per scope and directory.
- Guard the multi-project rename against the blur that Escape triggers.
- Unregister routes when disabling secondary projects.
- Replace the section message substring test with an explicit type set.
- Match an open session tab in the project search's current item and scope
selection acks by project id.
- Filter untrusted projects out of indexing consent, and restore config
scope switching plus project-scoped indexing writes that the consent
rework had removed.
- Round-trip the sessions-collapsed mutation so multi-project bodies, which
render purely from pushed state, reflect the toggle.
* fix(cli): enforce permissions on shell commands the parser fails to scan
* fix(cli): fail closed on error chunks without command names, move pwsh execution test to kilo file
Add an experimental multi-project mode to the Agent Manager sidebar behind
kilo-code.new.experimental.multiProject (default off). A persistent project
registry catalogs additional git repositories across restarts, while the
workspace repository stays the pinned default project.
Extension:
- Immutable per-project contexts own all repository-bound services (state,
worktrees, setup scripts, stale tracking, pollers) with generation-based
invalidation and fail-closed trust checks.
- ProjectContexts manage activation, expansion, and fast switching; session
routes resolve directories exactly per project via a shared route service.
- pushProjectSessions caches each project's session list and re-posts it on
fresh skips; session.created/updated/deleted SSE events upsert into the
owning project's cache so externally created sessions appear immediately.
- Selection restore persists the active target per project and falls back to
the local context silently when the remembered target is gone.
- Worktree lifecycle handlers extracted into provider-lifecycle.ts with an
explicit deps object instead of ambient project scope.
- initializeState and onRequestState always refresh sessions: with zero
managed sessions the listing never ran and the sidebar skeletons forever.
- Log instead of dropping silently when a state-gated message is not ready.
Webview:
- ProjectList accordion with per-project sidebar body, search, actions, and
default-branch dialog; selecting a project header restores its target.
- Local session tabs and terminal contexts are bucketed per project so open
tabs never leak across projects sharing the LOCAL context.
- The active project's session list overlays the live session store so new
sessions show without waiting for a backend re-list.
- SectionHeader requires a DragDropProvider ancestor; the multi-project body
now provides one (its absence crashed the whole webview render).
- SidebarBody and TabBar extracted out of AgentManagerApp (3215 to 2748
lines); AgentManagerProvider down to 1887 with caps lowered accordingly.
Also includes the config write revision bindings and per-project indexing
consent groundwork that rode along on this branch.
* test(cli): cover TUI startup outside package
* fix(cli): use native preload path in TUI test
---------
Co-authored-by: Johnny Eric Amancio <johnnyeric@gmail.com>
* fix(cli): drain session ingest queue on shutdown and flush terminal batches promptly
* fix(cli): drain the session ingest queue on process shutdown
* fix(cli): pin drain bound expiry, add changeset, conform to naming rule
* fix(cli): keep kilo-sessions out of the CLI startup import graph
* fix(cli): never let the ingest drain task reject the shutdown sequence
* test(cli): pin drain-before-dispose ordering on the KiloCli shutdown path
* fix(cli): make the guarded ingest drain non-rejecting and correct the lazy-import rationale
* test(cli): cover the retryable-status drain path under shutdown
* test(cli): decouple cli-shutdown drain assertions from declaration order
* fix(cli): advertise the instance from enableRemote so /remote registers as a spawn target
Enabling the remote relay from the TUI `/remote` slash command connected the
socket and mirrored sessions, but never advertised the instance, so the CLI
never appeared as a spawn target in the mobile "Run on" picker. Only the
explicit `kilo remote` command called setInstanceAdvertisement.
The advertisement now runs on every successful enableRemote() entry, before the
already-connected and coalescing early returns. That ordering matters: bootstrap
auto-enable frequently connects first, so `/remote` usually hits
`if (remote) return` and an advertisement placed in the connection-setup body
would leave the defect unfixed in the common case. `ingestDisabled` returns
before the advertisement and stays unadvertised.
The ensure helper is a no-op when an advertisement is already set, so it fires no
extra heartbeat, while explicit setInstanceAdvertisement keeps its existing
replace semantics. buildInstanceAdvertisement moves to a shared module so the
command path and the enable path derive it identically.
* fix(cli): report pending question and permission on the session heartbeat
The heartbeat built each session's status from SessionStatus.Service, whose
union is idle/retry/busy/offline and which never consults Question.Service or
Permission.Service. deriveStatus() already did consult both, but only fed the
ingest session_status sync. So a session genuinely blocked on a question was
advertised as busy on the heartbeat, and the mobile app — which takes live row
status from the heartbeat — showed no needs-input badge.
Extract the precedence (permission, then question, then SessionStatus) into a
shared helper used by both deriveStatus and the heartbeat, so the two channels
cannot drift.
The heartbeat runs on a ~10s timer across every session, and deriveStatus makes
service calls per session, so the permission and question lists are fetched once
per tick and indexed by session id rather than queried per session. A test pins
the call count.
Behaviour note beyond the strict fix: sharing the derivation also means a
SessionStatus of offline now reports as retry on the wire, matching what
deriveStatus has always sent to ingest. Nothing consumes offline from the
heartbeat — the transport forwards only idle and busy, and the mobile row treats
both as non-attention — so the effect is that the two channels now agree. The
detach fence test is parameterised accordingly; its assertion that the status
clears on detach is unchanged.
* chore(cli): widen the promise-facade allowlist for the heartbeat attention tests
The DEF-3 heartbeat tests raise and reply to real Question and Permission
requests through the global AppRuntime, which took kilo-sessions.test.ts from 4
classified references to 29 and failed the allowlist check.
Bumping the count rather than restructuring the tests is deliberate: the
heartbeat resolves attention status from the global Question.Service and
Permission.Service, so asserting it requires driving those same services.
Scoped layers cannot express that — the global-runtime coupling is the thing
under test — and it is the same integration pattern this entry already
sanctioned for the detach fence. The reason string records that.