* fix(ci): pass --auto to the docs-sync kilo runs and keep full stderr logs
Headless kilo run auto-rejects every permission ask it receives, and the
GitHub runner has no user config granting bash — so without --auto the
docs-sync bot's triage, edit and verify-fix calls were silently crippled
whenever the agent reached for a non-allowlisted shell command (CI run
30306629290: 9 rejections, all 11 edit batches failed, exit 0).
- Pass --auto immediately after "run" at all three call sites
(triage.mjs, edit.mjs, docs-sync.yml Fix verify failures step)
- runKilo now always writes the child's full stderr to
docs-sync-out/kilo-stderr-<label>.log, on success as well as failure —
the blindness that hid the defect
- selftest asserts --auto non-vacuously (region-scoped source match +
a stub invocation that records argv) and proves the stderr log is
written on both the failure and the summary-writing success path
* fix(cli): exit nonzero when a headless run auto-rejects or its session errors
Non-interactive kilo run reported success for runs that accomplished
nothing — a caller cannot distinguish success from a dead run, which is
why the docs-sync bot had to stop trusting exit codes entirely.
- Plain headless run (neither --auto nor --dangerously-skip-permissions)
in which the CLI auto-rejected at least one permission ask now exits
non-zero, even when the session still reaches idle afterwards, with
the diagnostic: run ended with an auto-rejected permission; pass
--auto for autonomous use. Deliberate contract change: any
auto-rejected ask means the run was crippled, not successful.
- A mid-stream session error now writes its diagnostic to stderr before
emitting the json error event, so the cause is visible under
--format json as well (the emit-first shape skipped the stderr write).
- The old exit-0 contract lock-in test is replaced: its llm.fail fixture
never published a consumable session.error, so it locked in a false
premise. New tests cover both scenarios in both output formats;
happy-path and --format json runs still exit 0 unchanged.
* fix(ci): redact secret env values from persisted kilo stderr logs
The full-stderr capture added for observability lands in uploaded CI
artifacts, which are raw files (GitHub masks secrets in log streams
only) on a public repo, and the runner env holds a long-lived
KILO_API_KEY. Redact exact values of KEY|TOKEN|SECRET-named env vars
(len >= 8) once at capture, so both the console tail and the artifact
file are safe. Adds the changeset for the headless exit-code contract
change.
* fix(ci): redact secrets from persisted kilo stdout and harden redaction
The docs-sync workflow uploads docs-sync-out/ as a public 14-day artifact,
and kilo stdout was persisted raw there in two more places: triage-raw-*.txt
and the edit-log.txt tee. Redact at capture in runKilo for stdout as already
done for stderr, and pipe the verify-fix step's kilo stdout through a new
line-wise redact-stream.mjs filter before tee.
Also harden redactEnvSecrets: widen the name pattern to
KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|ORG_ID|_PAT (KILO_ORG_ID is a repo
secret), replace longer values first so a short secret that prefixes a
longer one cannot leak the remainder, and document the exact-substring
limitation. Selftest gains cases 2e (prefix ordering), 2f (stdout capture),
2g (stream filter) and a non-vacuous exact-line assertion in 2d.
* fix(cli): correct the headless-exit changeset's json-format claim
The auto-reject path adds a new error event to the --format json stream;
only the existing event shapes are unchanged. Also state that the exit-1
rule covers a plain non-interactive --attach run that auto-rejects an ask.
* fix(ci): document --auto security trade-off and deferred hardening
Update comments in triage.mjs and edit.mjs to accurately describe the
security implications of --auto (unrestricted bash for an agent steered
by external PR content) and note that a scoped permission.bash map via
KILO_CONFIG_CONTENT is the intended hardening, deferred until required
shell patterns are stable.
* fix(ci): raise docs-sync budgets so the backlog can actually drain
--auto fixes the batches the bot attempted; it does not fix the ones it
never started. In run 30306629290 (254 PRs collected, 51 docs-worthy),
the wall-clock budgets deferred 54 PRs untriaged and 31 unedited without
an attempt — 45 of the 60 pending rows on the rolling PR. Triage got 8 of
11 chunks in 35 min; edit got 4 of 11 batches in 50 min.
Both are ceilings, not costs. A caught-up run needs ~2 chunks and ~1
batch and finishes in ~25 min, so raising them spends nothing on a normal
day and drains the backlog on a bad one. 90/120 covers 20 chunks and 14
batches — 500 triaged and 70 edited PRs against a ~5 docs-worthy/day
inflow — inside a 240-minute job timeout.
Also strip ANSI CSI sequences in tailText, the shared path both triage
and edit route their pending causes through. kilo renders its TUI to
stderr, so every "Why" cell on the rolling PR currently reads
"^[[0m→ ^[[0mRead packages/..." instead of the diagnostic. The persisted
docs-sync-out/kilo-stderr-*.log stays raw as the debugging record.
selftest case 2h asserts the pending reason is escape-free and still
carries the diagnostic text; case 2i asserts each budget fits at least
two units and the job timeout outlasts both, so a future edit cannot
silently restore a budget too small to run anything. Both shown failing
on the unmodified code first.
* fix(ci): keep the docs-sync rebuild authoritative in the fix step
The "Fix verify failures" step runs under `set -o pipefail` and the
default `bash -e`. Once the CLI half of this PR ships, `kilo run` exits 1
on a mid-stream session error, which aborts the block before the rebuild
runs: verify2.log is never written and `Re-verify status` reports
VERIFIED=false even when the docs build fine. A transient provider error
would flip every rolling docs PR to "Verification: failing".
The agent's exit code was never the signal for this step — the rebuild
is. Guard the pipeline with `|| echo ::warning::` so a nonzero kilo run
is surfaced but the rebuild still decides the outcome.
Extends selftest case 2b (which already parses this step) rather than
adding a case: the guard must appear between the pipeline's tee and the
rebuild, so a comment elsewhere in the block cannot satisfy it. Shown
failing with the guard removed.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.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.
* refactor(cli): run remote sessions in one process with safe per-session exit
Consolidate remote session handling into a single CLI process instead of
spawning one process per remote-created session (addresses the PR review):
- restore in-process create_session (accepts an absent sessionId and targets
the connection directory); remove the session spawner, the
KILO_REMOTE_ATTACH_SESSION attach-on-boot path, the child-advertisement gate,
and their tests
- retain instance advertisement and fire one immediate out-of-band heartbeat on
(re)connect when advertising, so a headless `kilo remote` host is discoverable
without delay
Make /exit (wire command exit_cli, unchanged for compatibility) detach only the
target session instead of terminating the CLI:
- AttachedState.detach with a presence-suppression tombstone; detach also clears
the target's SessionStatus so the negative-containment heartbeat fence resolves
deterministically for busy/retry/offline sessions
- exit_cli handler verifies ownership, cancels the active prompt, detaches and
awaits the detach heartbeat, then ACKs; the interactive RemoteExit callback is
invoked only after the ACK when the last owned session exits; a headless
`kilo remote` host stays alive and advertising at zero sessions
- add an optional canExitSession boolean to the list_commands v1 catalog
(always true, independent of exitAvailable) so clients can detect safe
session-exit semantics
History and stored sessions are preserved on exit.
* fix(cli): break module-load cycle in remote session prompt-cancel
The K1 in-process exit_cli seam added a static `import { SessionPrompt }`
to kilo-sessions.ts. @/session/prompt evaluates KiloSessionPrompt at module
load, so the new static edge raced that init and left the namespace in TDZ,
crashing unrelated test files with 'undefined is not an object (evaluating
KiloSessionPrompt.shouldAskPlanFollowup)'. Defer to a dynamic import at the
single call site, mirroring remote-command.ts.
* fix(cli): correct AttachedState announce/detach concurrency and rollback
Address review findings on the shared-process session lifecycle:
- announce/detach no longer join the OPPOSITE in-flight operation. Joining
detach's negative-containment fence made announce resolve success for a
detached id (and vice versa: detach joined announce and resolved success
while still attached, which exit_cli treats as license to ACK/close). Each
path now joins only a same-kind in-flight op and, when the opposite op is
in flight, awaits it to settle and then performs the real work.
- Failed-detach rollback now releases the suppression tombstone, so a
still-attached session is not dropped by the next setPresence (the tombstone
loop would otherwise remove the still-present id and never clear).
- Both catch/rollback branches now honor the lifecycle generation guard
(mirroring the success path); a stale in-flight op that rejects after
reset() no longer mutates the new lifecycle's presence/pending/suppressed
sets (reset clears the same Set instances).
Adds regression tests for each fix, plus AC6f covering the remote-ws
detachSessionId negative-containment waiter.
Confirm process-tree exit after SIGKILL on the non-Windows kill path, and
escalate SIGTERM to SIGKILL on the shutdown-hook (no-wait) path so a
SIGTERM-ignoring tree is not orphaned on JVM exit. Deflake the process-tree
kill test by capturing both children deterministically and add coverage for
the no-wait escalation.
Make JetBrains IDE app-close teardown non-blocking: send SIGTERM and return,
letting the JVM shutdown hook confirm exit, so quitting the IDE no longer
stalls the EDT for up to several seconds. Stop plugin-unload teardown from
waiting on the lifecycle mutex behind an in-flight download.
Add a parent-death watchdog to `kilo serve`: the VS Code extension and
JetBrains plugin pass their PID via KILO_PARENT_PID and the server exits when
that process disappears, covering hard client kills where no signal or
shutdown hook runs.
Reboots the per-directory instance, reloading config, skills, agents,
commands, and MCP prompts changed on disk without restarting the
server. Sessions and history are preserved; only the per-directory
instance caches are torn down and rebuilt.
Server: POST /instance/reload wraps the existing atomic
InstanceStore.reload path (the same one project.git.init uses). The
rebuild completes before the 200 response, so clients can refetch with
no race. Returns 409 ConflictError while a session is actively
running. Emits the existing server.instance.disposed SSE event, which
the TUI and extension already use to auto-refetch.
CLI: /reload palette command calls the endpoint; the TUI already
bootstraps on server.instance.disposed.
Extension: /reload slash command, a reload button in the task header
and settings panel, and a Kilo Code: Reload Config and Skills command
palette entry. The handler clears the command cache and reuses
reloadAfterAuthChange to re-fetch config, providers, agents, skills,
and commands. Reload targets the current session's directory so Agent
Manager worktree sessions reload their own worktree instance rather
than the workspace root.
SDK: regenerate so client.instance.reload is available to external
integrations.
* feat(memory): opt-in project memory — capture, recall, CLI + TUI integration
Add project memory: the standalone @kilocode/kilo-memory effect layer plus the
opencode CLI/server/TUI integration. Memory is disabled by default, so it is a
no-op until enabled (no behavior change when off).
Capture (turn-close consolidation): per-op parse salvage, secret redaction that
skips the offending op instead of aborting the batch, supersede-only auto-updates
(never model-driven deletes), correction-aware echo handling, non-LLM fallback
digests on interrupted/error turns, a shared interval throttle with idle-flush.
Recall + injection: keyword tokenizer with camelCase/compound splitting, light
stemming, and an English-first stopword filter (Unicode-aware; non-English falls
back to plain token-overlap), a live relevance floor, a budget-reserved startup
index, a session-digest catalog, and per-session prompt-cache pinning of the
injected memory block.
Surfaces: kilo_memory_save / kilo_memory_recall tools, the memory HTTP API
(contract schemas live in the package), and a status-focused TUI sidebar showing
auto-save, loaded context, and active recall, plus the /memory dialog.
* fix(memory): address PR review feedback
- C1: bump @kilocode/kilo-memory in the changeset
- C2: redact secrets before they hit the audit log (skip + salvage paths);
redact before truncating in salvageTyped so a secret straddling the
500-char cap can't leak an unmatched fragment; opText -> salvageText
- C3: de-abbreviate savedOperations, "changes" wording, ops.ts -> operations.ts
- C4: log.warn on the remaining silent-catch fallbacks (turn diff, memory
context injection, tool-visibility check)
- C5: relocate memory storage from ~/.kilo to Global.Path.data, delete the
now-dead needsDependencyInstall guard, add /memory status (root path) and
/memory edit ($VISUAL/$EDITOR + auto-rebuild)
- C6: replace the hardcoded English stopword list with corpus-derived
ubiquitous-term filtering (df across the user's own entries) and the
English suffix stemmer with suffix-tolerant term matching, so recall
noise-filtering works in any language
- C8: delete the CORRECTION_INTENT English regex; echo turns now run typed
capture (digest stays echo-gated), bounded by the interval throttle, with
the typed prompt as the language-agnostic content filter
- C9: exclude generated paths (dist/build/coverage/*.gen.*/*.map/snapshots)
from the durable-diff churn fallback so generated churn can't burn a
consolidation call
- C11: fix duplicated assert in httpapi-memory test; assert the error body
- kilo-code-bot batch: clause-boundary regex fix, byte-safe catalog
truncation, max-length guards on remember/correct/forget payloads (text,
query, key, sessionID), trim consistency in reconcile, param-shadowing
rename, missing doc entry for kilo_memory_recall, dead-code removal,
dialog UI fixes, memoryEnabledCache eviction bound, dedicated Configure
schema, recall permission renderer, covered-session pointer cap, redact
chat transcript before the consolidation model call, split configProtected
metadata from disableAlways so memory-save prompts don't show config-file
copy, drop unused MemoryService.layer provide from tool registry
- redact colon-separated low-entropy secrets too (password: hunterx),
accepting the prose false-positive tradeoff (secret: enabled) in favor of
not missing a real secret
- rename lastConsolidatedAt -> lastTypedConsolidationAt to make its narrow
scope (typed-consolidation throttle clock) explicit; regen openapi/SDK
- drop now-dead home/config fields from MemoryPaths.Host after the data-dir
relocation; add Process.splitCommand for quoted $EDITOR/$VISUAL paths with
spaces, used by /memory edit and the pre-existing Editor.open utility
* refactor(memory): shared client helpers, capture hardening, /memory UX rework
- extract client-side derivations into kilo-memory so both frontends share
one implementation: MemoryDecisions.summarize (decision-log summary),
MemoryAutosaveStatus.summarize (autosave-status semantics), and
MemoryMarkerMeta (marker wire contract encode/decode)
- match exact-key upserts via the canonical stored id (slugged key,
normalized section) so a re-emitted spaced/uppercase key updates the
entry instead of falling to fuzzy dedupe
- salvageTyped throws on valid JSON without an operations array so the
caller's fallback path records a parse error instead of a silent
zero-op success
- rename memory tool metadata files -> sources (stripPartMetadata rewrites
tool-part metadata.files assuming apply_patch records, mangling string[])
- read state instead of status for tool enabled checks; dedupe TUI helpers
(errorMessage, shared route(), Locale.number, relativeTime)
- /memory UX: bare /memory opens a help modal driven by a structured
command catalog in kilo-memory; /memory on|off become the canonical
toggle verbs (enable/disable kept as quiet aliases); /memory status opens
a clean overview dialog (root path, autosave, startup context, source
counts, index size) instead of a toast; /memory show is the single full
audit view (inspect removed)
Move the vim modal editing engine and prompt integration out of shared
upstream files and into src/kilocode/ mirrors per the Fork Isolation Rule:
- src/kilocode/cli/cmd/tui/component/prompt/vim.ts (engine, moved)
- src/kilocode/cli/cmd/tui/component/prompt/index.tsx (new mirror with
useVim, VimModeIndicator, vimToggleCommand)
- test/kilocode/cli/cmd/tui/prompt/vim.test.ts (moved)
The shared prompt/index.tsx now calls into the mirror behind minimal
kilocode_change markers instead of inlining ~200 lines of vim logic.