Skip to content

feat(harness-sdk): the kilo harness SDK - #5878

Merged
iscekic merged 8 commits into
mainfrom
feat/kilo-harness-sdk
Oct 10, 2026
Merged

iscekic merged 8 commits into
mainfrom
feat/kilo-harness-sdk

Conversation

@iscekic

@iscekic iscekic commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

A new package, @kilocode/harness-sdk. It holds a conversation with a model,
keeps the model cache warm, stores the conversation, and summarises it when the
conversation outgrows the window.

One source runs on Node and on React Native. Nothing in src/core/ names a
platform: fetch, SQLite and the source of random bytes are all plugin points.

Read packages/harness-sdk/README.md first. It is what a consumer sees, and
it is the fastest way to judge whether the shape is right. AGENTS.md is the
second read: it holds why each decision was made and the measurement behind it.

Nothing on this branch imports the package, so it cannot change any behaviour
that runs today. #5895 is stacked on it and is the first consumer: the mobile
chat tab.

What it does

  • Holds a session. openSession gives a handle. ask streams the answer
    back as events: text, thinking, tool calls, tool results, and a done that
    carries the token counts and why the model stopped.
  • Speaks three gateway shapes. messages, responses and chat_completions,
    from one internal model. A model the catalog knows nothing about is assumed to
    speak all three, and the best one it actually speaks is used.
  • Keeps the prefix stable. The prompt is append-only and the cache
    breakpoints do not move, which is what makes the provider's cache hit.
  • Runs tools between rounds. Every call can outlive the request: the model is
    told a call is still running, carries on with what does not depend on it, and
    the answer drives a round of its own when it lands. Four parties decide how
    long it waits, in a fixed order.
  • Ships two tools. questionTool asks the person; subagentTool hands a
    task to a session of its own and returns one answer.
  • Stores and resumes. SQLite through a one-function driver, on node:sqlite
    or expo-sqlite. A session can be continued, or cloned onto another model —
    which is how a person switches model mid-conversation without losing it.
    The store plugins name no runtime type, so the package depends on neither.
  • Compacts itself. A session that fills the window summarises itself and
    keeps what it was told to keep.

How it is built

src/core/ holds the contracts and the pure domain. src/plugins/ holds every
implementation, including the package's own defaults. A CI check asserts the
direction: core/ never imports plugins/.

Eight plugin points, each a Context.Tag with a default this package owns:
ModelClient, ToolRegistry, PromptAssembler, ModelCatalog, SessionStore,
TokenSource, RetryPolicy, EntropySource. layerKilo is the wiring almost
every caller writes; each part underneath is still swappable.

Nine entry points, so a consumer bundles only what they run. The conformance
checks and the shipped fetch adapter each sit behind one of their own.

Libraries do the work: effect for control flow, streams and resources,
eventsource-parser for SSE, typia for validation, and the Anthropic and
OpenAI SDKs as types only, so a wrong field name in a request body does not
compile. One exception is written down with its reason: core/id.ts encodes a
ULID itself, because every ulid build either drags node:crypto into the core
or throws at import on a runtime with no global crypto.

typia runs at compile time through ttsc, so no schema object exists at run
time and validation costs 0.005 us per event.

What proves it

Unit tests 255 across 42 files
Live runs against the real gateway 20, pnpm test:e2e:all
Timing gates pnpm test:perf
Plugin conformance checkStore, checkAssembler, shipped for plugin authors
Package surface check:package imports the built bundle from all 9 entry points
Platform check:platform fails the build if it names a runtime
Migrations check:migrations catches the inlined SQL drifting from the schema

The live runs are not in CI and must not be: they cost money and need a token.
They exist because a change to the wire or to the session has to be answered by
the provider, not by a fake.

Two matrices are recorded in AGENTS.md rather than described:

  • Ten models, five turns each. Every one answered the last question from
    history. A model whose provider caches at all reads above 0.99 of its input
    from the cache.
  • Eleven models, both shipped tools. Every one called each tool and sent a
    payload the schema accepted. The subagent description was tuned against this
    table until it did — two models had read the old text as naming the only two
    uses, and answered that no tool of theirs could look a thing up.

Timing, measured rather than assumed: a 200 turn prompt assembles in under 20 us,
a whole request costs 48 us before the socket, and a streamed token costs 7 us
end to end against seconds of provider latency. AGENTS.md also records one
optimisation that was expected to be ten-fold and measured eleven percent, and
one memory measurement that was thrown away because it moved more than the thing
it was measuring.

Outside the package

  • .github/workflows/ci.yml — a harness-sdk job, path-filtered, running
    check:ci. The migration check is why it exists: nothing at run time notices
    when the inlined SQL stops matching the schema.
  • .gitignore — the package's dist/.
  • pnpm-lock.yaml — the new dependencies.
  • apps/web/AGENTS.md — ten lines next dev writes into the file itself and
    re-adds when removed. Unrelated to this package; committed because that is
    what the block asks for.

Not here

@iscekic iscekic self-assigned this Sep 4, 2026
@iscekic
iscekic marked this pull request as ready for review September 5, 2026 04:03
Comment thread packages/harness-sdk/src/core/ask.ts Resolved
@kilo-code-bot

kilo-code-bot Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

No new issues: the PR-owned packages/harness-sdk tree is byte-identical to the previously reviewed revision (4c7df0a), and the only deltas in PR files since then are a generated lockfile rebuild and main-merge changes owned by other PRs.

Files Reviewed (6 files)
  • packages/harness-sdk/src/core/ask.ts — unchanged since 4c7df0a; prior comment disproven by author's Effect.onError proof and regression test
  • packages/harness-sdk/src/core/background.ts — wake fix present
  • packages/harness-sdk/src/core/compact.ts — cacheKey/effort fix present
  • packages/harness-sdk/src/plugins/gateway/wire/responses.ts — response.incomplete fix present
  • packages/harness-sdk/src/plugins/store/driver.ts — write-line fix present
  • packages/harness-sdk/e2e/tools.ts — twice/Effect.either fix present
Incremental scope

git diff 4c7df0affa69bb0611068d5596a75b436a771127..HEAD -- packages/harness-sdk is empty (tree 1650d470b at both revisions). The incremental range is otherwise dominated by the merge of origin/main; the PR's own file set versus base (ec82391fe) is 175 files, of which only pnpm-lock.yaml (generated, skipped) changed after the previous review.

Previous Review Summaries (10 snapshots, latest commit 4c7df0a)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 4c7df0a)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/harness-sdk/src/plugins/gateway/wire/completions.ts
  • packages/harness-sdk/src/plugins/gateway/wire/replay.test.ts

Previous review (commit 1b6d23f)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/harness-sdk/src/plugins/gateway/wire/completions.ts 219 Empty content: "" is treated as a text delta and shadows reasoning on the same frame
Files Reviewed (14 files)
  • packages/harness-sdk/src/plugins/gateway/wire/completions.ts - 1 issue
  • packages/harness-sdk/src/plugins/gateway/wire/responses.ts
  • packages/harness-sdk/src/plugins/gateway/wire/messages.ts
  • packages/harness-sdk/src/plugins/gateway/index.ts
  • packages/harness-sdk/src/plugins/gateway/http.ts
  • packages/harness-sdk/src/core/ask.ts
  • packages/harness-sdk/src/core/background.ts
  • packages/harness-sdk/src/core/loop.ts
  • packages/harness-sdk/src/core/compact.ts
  • packages/harness-sdk/src/core/queue.ts
  • packages/harness-sdk/src/plugins/store/driver.ts
  • packages/harness-sdk/src/plugins/store/sqlite.ts
  • packages/harness-sdk/src/plugins/tools/subagent.ts
  • packages/harness-sdk/e2e/tools.ts

Fix these issues in Kilo Cloud

Previous review (commit b1600c9)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/harness-sdk/e2e/compact.ts
  • packages/harness-sdk/e2e/together.ts

Previous review (commit c5808bf)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • packages/harness-sdk/e2e/cancel.ts
  • packages/harness-sdk/e2e/conversation.ts
  • packages/harness-sdk/e2e/live.ts
  • packages/harness-sdk/e2e/shapes.ts
  • packages/harness-sdk/e2e/subagent.ts
  • packages/harness-sdk/e2e/tools.ts

Previous review (commit 7556919)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • packages/harness-sdk/e2e/image.ts

Previous review (commit 7dc528e)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/harness-sdk/e2e/tools.ts
  • packages/harness-sdk/e2e/queue.ts

Previous review (commit 7fe69c0)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/harness-sdk/e2e/tools.ts 325 First-attempt fail() makes the retry a no-op
Files Reviewed (4 files)
  • packages/harness-sdk/e2e/tools.ts - 1 issue
  • packages/harness-sdk/e2e/cancel.ts
  • packages/harness-sdk/e2e/image.ts
  • packages/harness-sdk/e2e/subagent.ts

Fix these issues in Kilo Cloud

Previous review (commit 4766987)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/harness-sdk/e2e/tools.ts 316 First-attempt fail() makes the retry a no-op
Files Reviewed (5 files)
  • packages/harness-sdk/e2e/tools.ts - 1 issue
  • packages/harness-sdk/e2e/cancel.ts
  • packages/harness-sdk/e2e/image.ts
  • packages/harness-sdk/e2e/subagent.ts
  • packages/harness-sdk/e2e/together.ts

Fix these issues in Kilo Cloud

Previous review (commit dfc1b09)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/harness-sdk/e2e/tools.ts 296 First-attempt fail() makes the retry a no-op
Files Reviewed (9 files)
  • packages/harness-sdk/e2e/tools.ts - 1 issue
  • packages/harness-sdk/AGENTS.md
  • packages/harness-sdk/e2e/cancel.ts
  • packages/harness-sdk/e2e/clone.ts
  • packages/harness-sdk/e2e/conversation.ts
  • packages/harness-sdk/e2e/image.ts
  • packages/harness-sdk/e2e/queue.ts
  • packages/harness-sdk/e2e/subagent.ts
  • packages/harness-sdk/e2e/together.ts

Fix these issues in Kilo Cloud

Previous review (commit 1f53424)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/harness-sdk/e2e/tools.ts 296 First-attempt fail() makes the retry a no-op
Files Reviewed (7 files)
  • packages/harness-sdk/e2e/tools.ts - 1 issue
  • packages/harness-sdk/AGENTS.md
  • packages/harness-sdk/e2e/cancel.ts
  • packages/harness-sdk/e2e/image.ts
  • packages/harness-sdk/e2e/queue.ts
  • packages/harness-sdk/e2e/subagent.ts
  • packages/harness-sdk/e2e/together.ts

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

Comment thread packages/harness-sdk/e2e/tools.ts Resolved
@iscekic iscekic added human-ready The PR is ready for human review. and removed human-ready The PR is ready for human review. labels Sep 6, 2026
@iscekic
iscekic marked this pull request as draft September 10, 2026 00:54
iscekic added a commit that referenced this pull request Sep 10, 2026
@iscekic
iscekic force-pushed the feat/kilo-harness-sdk branch from 2e6d972 to 1b6d23f Compare September 10, 2026 01:38
@iscekic
iscekic marked this pull request as ready for review September 10, 2026 01:52
@iscekic
iscekic marked this pull request as draft September 10, 2026 02:16
@iscekic

iscekic commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Accepted. Pushed 4c7df0a (kwf kwf-fix-review-42c5).

What changed:

  • packages/harness-sdk/src/plugins/gateway/wire/completions.ts:219 — accepted. toDelta returned a text delta for content: "" before it reached the reasoning branch, so an empty placeholder shadowed the thinking on the same frame. Now only a non-empty string counts as a text delta. An empty-content frame yields its reasoning (reasoning or reasoning_content), or undefined when it carries nothing else. A replay unit test covers both reasoning fields and the bare empty frame.

@iscekic
iscekic force-pushed the feat/kilo-harness-sdk branch from 1b6d23f to 4c7df0a Compare September 10, 2026 02:48
@iscekic
iscekic marked this pull request as ready for review September 10, 2026 03:04
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 10, 2026

@eshurakov eshurakov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after a shallow triage pass. No blocking findings.

@iscekic iscekic added the merge-by-human the merge bot routed this PR to a human label Sep 21, 2026
@iscekic
iscekic removed the request for review from Drixled September 21, 2026 11:39
# Conflicts:
#	.github/workflows/ci.yml
#	pnpm-lock.yaml
The merge rebuilt the lockfile from the branch side, so pnpm resolved again
every package main had added since. That moved @peculiar/asn1-* to 2.9.5 and
broke the passkey verification tests in apps/web. The lockfile is now main's,
with only the harness SDK's own entries added.
@iscekic iscekic removed the human-ready The PR is ready for human review. label Sep 27, 2026
# Conflicts:
#	.github/workflows/ci.yml
#	pnpm-lock.yaml
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​ttsc/​unplugin@​0.28.38310010096100
Addednpm/​ttsc@​0.28.38810010096100
Addednpm/​effect@​3.22.1931009197100
Addednpm/​typia@​14.0.410010010096100

View full report

@iscekic
iscekic merged commit a0cd834 into main Oct 10, 2026
31 checks passed
@iscekic
iscekic deleted the feat/kilo-harness-sdk branch October 10, 2026 23:09
iscekic added a commit that referenced this pull request Oct 11, 2026
…uick Chat (#7338)

* feat(harness-sdk): the kilo harness SDK

#5878

* feat(mobile): rebuild the chat tab on the harness SDK

#5895

* chore(kilo-mcp): refresh catalog.json from CI

* feat(mobile): connect chat to the Kilo MCP server with the signed-in session (#6420)

#6420

* chore: publish the verified app-agent settings tools + remote MCP stack as one PR (level 1)

* fix(mobile): preserve settings and remote MCP state across cold loads

* fix(mobile): satisfy async and control-flow lint rules

* fix(harness-sdk): keep the library's abort link until the operation ends

fetch resolves when the headers arrive, so the per-request unlink let close()
leave an open stream running until the deadline timer fired. The deadline now
holds each joined signal once and removes it at stop.

* fix(harness-sdk): bound the credential by the operation's deadline

The token was asked before the deadline existed, so a slow refresh could hold
a chat open past the discovery bound. The deadline now comes first and the
credential races it.

* fix(harness-sdk): give a call a deadline above the time it is waited on inline

A remote tool is waited on for 15 s before the session backgrounds it, and the
call's own default deadline was also 15 s, so a backgrounded call failed at
that moment instead of answering. A call now defaults to 60 s; discovery keeps
15 s.

* fix(mobile): publish the Kilo tools answer that a cache hit serves

The registry builds a chat from the snapshot, not from the return value. A
cache hit returned the ready answer while another scope's discovery had
published connecting over it, so the chat opened on the base tools.

* fix(mobile): keep each Kilo tools discovery on its caller's deadline

A caller that joined a running discovery took the first caller's deadline, so
an open that met a Retry waited fifteen seconds and a Retry that met an open
was cut to four. A caller now joins only a discovery with its own deadline,
and only the latest discovery is published.

* fix(mobile): lock the Kilo tools switch when there is nothing to use

In the unavailable state the switch read off while the chat's setting was on,
so a tap wrote the value already stored and the switch snapped back. The view
now says whether the switch can move, and the sheet disables it when not.

* fix(deps): rebuild the lockfile on main's pins

The merge rebuilt the lockfile from the branch side, so pnpm resolved again
every package main had added since. That moved @peculiar/asn1-* to 2.9.5 and
broke the passkey verification tests in apps/web. The lockfile is now main's,
with only the harness SDK's own entries added.

* fix(mobile): pass the segments to the toaster's tab bar check

Main added a shouldHideTabBar call in app-root-providers.tsx with the old
one-argument form. This branch reads the chat conversation from the segments,
so the call now passes them too.

* test(mobile): read the hoisted query client mock in the account switch test

Main's lifecycle file already holds queryClientMock at module scope. The
branch's test declared it again from a dynamic import, which lint refuses as a
shadow and as an unbound method.

* fix(mobile): describe the spend alerts notification setting

Main added the spendAlerts notification category. The agent settings catalog
maps every category to a description, so typecheck failed on the missing one.

* fix(mobile): use the shared MCP servers copy in the chat sheet

Main added profiles.mcp.title and profiles.mcp.add with the same English copy
as modelChat.mcp.serversSection and modelChat.mcp.addServer, and the catalog
check refuses two keys for one copy. The chat section now reads the profile
keys, and the two chat keys are removed from every catalog.

* test(mobile): load the real config module in the MCP URL test

vitest.setup.ts now stubs @/lib/config for every pure suite, so the plain
dynamic import in config.test.ts returned the stub and KILO_MCP_URL read as
undefined. The test now loads the module through vi.importActual.

* test(mobile): allow the remote MCP id slug in the case guard

slugFor in remote-mcp-store.ts lowercases a server name to build the callable
tool-name id. The id is never shown, so the file joins the non-display list.

* feat(mobile): support configurable Quick Chat backends

* test(harness): construct synthetic credential URL explicitly

* fix(mobile): address Quick Chat backend review findings

* fix(mobile): reopen a failed chat from Retry

* test(mobile): allow protocol-identifier case normalization in backend files

* fix(mobile): wait for stored backends before the send check

* feat(mobile): add on-device Apple Foundation Models to Quick Chat

* feat(mobile): add on-device Gemini Nano to Quick Chat on Android

* fix(mobile): reference the Retry label in the on-device background copy

* feat(mobile): add llama.rn GGUF models to Quick Chat

* style(mobile): format the on-device GGUF files

* test(mobile): allow the GGUF file-extension comparison in the case guard

* fix(mobile): keep unreadable model entries from deleting downloads

* test(mobile): split the model-index policy tests into their own suite

* fix(mobile): never rewrite a model list holding an unreadable entry

* fix(mobile): keep any list that did not parse in full from being rewritten

* fix(mobile): read a GGUF result that carries nulls or only text

* fix(mobile): treat a null parsed content as absent

* test(mobile): pin the null-heavy llama.rn result at the client level

* feat(mobile): open message details and recover a failed question in Quick Chat

A long-press on a Quick Chat message opens the message details sheet.
The unanswered question carries Retry and Copy to composer inline.
A failed answer reads as a failed response, not as a stopped message.
The chat row under the transcript states the reason and keeps its Retry
only when no question waits for an answer.

Quick Chat messages carry no model and no time. A model switch copies
every turn into a new session, so the details sheet omits those rows.

* fix(mobile): drop the duplicate llama.rn proguard rule

The merge of main added a second extraProguardRules next to main's combined
rule, which app.config.ts rejects as a duplicate key. Keep main's combined rule.

* ci(trufflehog): exclude the synthetic-credential test fixture

security.test.ts builds a credential URL from the literal
'user:private-key@provider.example'. The commit that added it stays in this
branch's history, so trufflehog reports it on every base..head scan even though
a later commit replaced it with a constructed value. Exclude the fixture path.

* fix(db): drop the stale duplicate 0263_drop_quick_chat migration

main lands the drop as 0274_drop_quick_chat.sql (registered in the journal). The
same drop sits in this branch's lineage as 0263_drop_quick_chat.sql, unregistered
and colliding with 0263_spend_alerts_rollup_covering_index. The merge kept it
because main never had that path. Remove it; the tree now matches main.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
iscekic added a commit that referenced this pull request Oct 11, 2026
* feat(harness-sdk): the kilo harness SDK

#5878

* feat(mobile): rebuild the chat tab on the harness SDK

#5895

* chore(kilo-mcp): refresh catalog.json from CI

* feat(mobile): connect chat to the Kilo MCP server with the signed-in session (#6420)

#6420

* chore: publish the verified app-agent settings tools + remote MCP stack as one PR (level 1)

* fix(mobile): preserve settings and remote MCP state across cold loads

* fix(mobile): satisfy async and control-flow lint rules

* fix(harness-sdk): keep the library's abort link until the operation ends

fetch resolves when the headers arrive, so the per-request unlink let close()
leave an open stream running until the deadline timer fired. The deadline now
holds each joined signal once and removes it at stop.

* fix(harness-sdk): bound the credential by the operation's deadline

The token was asked before the deadline existed, so a slow refresh could hold
a chat open past the discovery bound. The deadline now comes first and the
credential races it.

* fix(harness-sdk): give a call a deadline above the time it is waited on inline

A remote tool is waited on for 15 s before the session backgrounds it, and the
call's own default deadline was also 15 s, so a backgrounded call failed at
that moment instead of answering. A call now defaults to 60 s; discovery keeps
15 s.

* fix(mobile): publish the Kilo tools answer that a cache hit serves

The registry builds a chat from the snapshot, not from the return value. A
cache hit returned the ready answer while another scope's discovery had
published connecting over it, so the chat opened on the base tools.

* fix(mobile): keep each Kilo tools discovery on its caller's deadline

A caller that joined a running discovery took the first caller's deadline, so
an open that met a Retry waited fifteen seconds and a Retry that met an open
was cut to four. A caller now joins only a discovery with its own deadline,
and only the latest discovery is published.

* fix(mobile): lock the Kilo tools switch when there is nothing to use

In the unavailable state the switch read off while the chat's setting was on,
so a tap wrote the value already stored and the switch snapped back. The view
now says whether the switch can move, and the sheet disables it when not.

* fix(deps): rebuild the lockfile on main's pins

The merge rebuilt the lockfile from the branch side, so pnpm resolved again
every package main had added since. That moved @peculiar/asn1-* to 2.9.5 and
broke the passkey verification tests in apps/web. The lockfile is now main's,
with only the harness SDK's own entries added.

* fix(mobile): pass the segments to the toaster's tab bar check

Main added a shouldHideTabBar call in app-root-providers.tsx with the old
one-argument form. This branch reads the chat conversation from the segments,
so the call now passes them too.

* test(mobile): read the hoisted query client mock in the account switch test

Main's lifecycle file already holds queryClientMock at module scope. The
branch's test declared it again from a dynamic import, which lint refuses as a
shadow and as an unbound method.

* fix(mobile): describe the spend alerts notification setting

Main added the spendAlerts notification category. The agent settings catalog
maps every category to a description, so typecheck failed on the missing one.

* fix(mobile): use the shared MCP servers copy in the chat sheet

Main added profiles.mcp.title and profiles.mcp.add with the same English copy
as modelChat.mcp.serversSection and modelChat.mcp.addServer, and the catalog
check refuses two keys for one copy. The chat section now reads the profile
keys, and the two chat keys are removed from every catalog.

* test(mobile): load the real config module in the MCP URL test

vitest.setup.ts now stubs @/lib/config for every pure suite, so the plain
dynamic import in config.test.ts returned the stub and KILO_MCP_URL read as
undefined. The test now loads the module through vi.importActual.

* test(mobile): allow the remote MCP id slug in the case guard

slugFor in remote-mcp-store.ts lowercases a server name to build the callable
tool-name id. The id is never shown, so the file joins the non-display list.

* feat(mobile): support configurable Quick Chat backends

* test(harness): construct synthetic credential URL explicitly

* fix(mobile): address Quick Chat backend review findings

* fix(mobile): reopen a failed chat from Retry

* test(mobile): allow protocol-identifier case normalization in backend files

* fix(mobile): wait for stored backends before the send check

* feat(mobile): add on-device Apple Foundation Models to Quick Chat

* feat(mobile): add on-device Gemini Nano to Quick Chat on Android

* fix(mobile): reference the Retry label in the on-device background copy

* feat(mobile): add llama.rn GGUF models to Quick Chat

* style(mobile): format the on-device GGUF files

* test(mobile): allow the GGUF file-extension comparison in the case guard

* fix(mobile): keep unreadable model entries from deleting downloads

* test(mobile): split the model-index policy tests into their own suite

* fix(mobile): never rewrite a model list holding an unreadable entry

* fix(mobile): keep any list that did not parse in full from being rewritten

* fix(mobile): read a GGUF result that carries nulls or only text

* fix(mobile): treat a null parsed content as absent

* test(mobile): pin the null-heavy llama.rn result at the client level

* fix(mobile): remove the mode chip from the Quick Chat composer

Quick Chat is not an agent session, so modes do not apply. The toolbar and
the composer take one optional modeControl object. Without it, the toolbar
draws no mode chip and the model chip starts the row.

* test(mobile): pin the mode control on the session composer

* fix(mobile): drop the duplicate llama.rn proguard rule

The merge of main added a second extraProguardRules next to main's combined
rule, which app.config.ts rejects as a duplicate key. Keep main's combined rule.

* ci(trufflehog): exclude the synthetic-credential test fixture

security.test.ts builds a credential URL from the literal
'user:private-key@provider.example'. The commit that added it stays in this
branch's history, so trufflehog reports it on every base..head scan even though
a later commit replaced it with a constructed value. Exclude the fixture path.

* fix(db): drop the stale duplicate 0263_drop_quick_chat migration

main lands the drop as 0274_drop_quick_chat.sql (registered in the journal). The
same drop sits in this branch's lineage as 0263_drop_quick_chat.sql, unregistered
and colliding with 0263_spend_alerts_rollup_covering_index. The merge kept it
because main never had that path. Remove it; the tree now matches main.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
iscekic added a commit that referenced this pull request Oct 11, 2026
* feat(harness-sdk): the kilo harness SDK

#5878

* feat(mobile): rebuild the chat tab on the harness SDK

#5895

* chore(kilo-mcp): refresh catalog.json from CI

* feat(mobile): connect chat to the Kilo MCP server with the signed-in session (#6420)

#6420

* chore: publish the verified app-agent settings tools + remote MCP stack as one PR (level 1)

* fix(mobile): preserve settings and remote MCP state across cold loads

* fix(mobile): satisfy async and control-flow lint rules

* fix(harness-sdk): keep the library's abort link until the operation ends

fetch resolves when the headers arrive, so the per-request unlink let close()
leave an open stream running until the deadline timer fired. The deadline now
holds each joined signal once and removes it at stop.

* fix(harness-sdk): bound the credential by the operation's deadline

The token was asked before the deadline existed, so a slow refresh could hold
a chat open past the discovery bound. The deadline now comes first and the
credential races it.

* fix(harness-sdk): give a call a deadline above the time it is waited on inline

A remote tool is waited on for 15 s before the session backgrounds it, and the
call's own default deadline was also 15 s, so a backgrounded call failed at
that moment instead of answering. A call now defaults to 60 s; discovery keeps
15 s.

* fix(mobile): publish the Kilo tools answer that a cache hit serves

The registry builds a chat from the snapshot, not from the return value. A
cache hit returned the ready answer while another scope's discovery had
published connecting over it, so the chat opened on the base tools.

* fix(mobile): keep each Kilo tools discovery on its caller's deadline

A caller that joined a running discovery took the first caller's deadline, so
an open that met a Retry waited fifteen seconds and a Retry that met an open
was cut to four. A caller now joins only a discovery with its own deadline,
and only the latest discovery is published.

* fix(mobile): lock the Kilo tools switch when there is nothing to use

In the unavailable state the switch read off while the chat's setting was on,
so a tap wrote the value already stored and the switch snapped back. The view
now says whether the switch can move, and the sheet disables it when not.

* fix(deps): rebuild the lockfile on main's pins

The merge rebuilt the lockfile from the branch side, so pnpm resolved again
every package main had added since. That moved @peculiar/asn1-* to 2.9.5 and
broke the passkey verification tests in apps/web. The lockfile is now main's,
with only the harness SDK's own entries added.

* fix(mobile): pass the segments to the toaster's tab bar check

Main added a shouldHideTabBar call in app-root-providers.tsx with the old
one-argument form. This branch reads the chat conversation from the segments,
so the call now passes them too.

* test(mobile): read the hoisted query client mock in the account switch test

Main's lifecycle file already holds queryClientMock at module scope. The
branch's test declared it again from a dynamic import, which lint refuses as a
shadow and as an unbound method.

* fix(mobile): describe the spend alerts notification setting

Main added the spendAlerts notification category. The agent settings catalog
maps every category to a description, so typecheck failed on the missing one.

* fix(mobile): use the shared MCP servers copy in the chat sheet

Main added profiles.mcp.title and profiles.mcp.add with the same English copy
as modelChat.mcp.serversSection and modelChat.mcp.addServer, and the catalog
check refuses two keys for one copy. The chat section now reads the profile
keys, and the two chat keys are removed from every catalog.

* test(mobile): load the real config module in the MCP URL test

vitest.setup.ts now stubs @/lib/config for every pure suite, so the plain
dynamic import in config.test.ts returned the stub and KILO_MCP_URL read as
undefined. The test now loads the module through vi.importActual.

* test(mobile): allow the remote MCP id slug in the case guard

slugFor in remote-mcp-store.ts lowercases a server name to build the callable
tool-name id. The id is never shown, so the file joins the non-display list.

* feat(mobile): support configurable Quick Chat backends

* test(harness): construct synthetic credential URL explicitly

* fix(mobile): address Quick Chat backend review findings

* fix(mobile): reopen a failed chat from Retry

* test(mobile): allow protocol-identifier case normalization in backend files

* fix(mobile): wait for stored backends before the send check

* feat(mobile): add on-device Apple Foundation Models to Quick Chat

* feat(mobile): add on-device Gemini Nano to Quick Chat on Android

* fix(mobile): reference the Retry label in the on-device background copy

* feat(mobile): add llama.rn GGUF models to Quick Chat

* style(mobile): format the on-device GGUF files

* test(mobile): allow the GGUF file-extension comparison in the case guard

* fix(mobile): keep unreadable model entries from deleting downloads

* test(mobile): split the model-index policy tests into their own suite

* fix(mobile): never rewrite a model list holding an unreadable entry

* fix(mobile): keep any list that did not parse in full from being rewritten

* fix(mobile): read a GGUF result that carries nulls or only text

* fix(mobile): treat a null parsed content as absent

* test(mobile): pin the null-heavy llama.rn result at the client level

* feat(mobile): send images to Quick Chat models that read them

Quick Chat accepts images from the picker and the clipboard when the
selected model reads images. The images stay on the device: the composer
reads them as bounded JPEG base64 and hands them to the send path. The
Cloud Agent upload path does not change.

Capability contract:
- backend-capabilities.ts: targetSupportsImages(targetId),
  resolvedTargetSupportsImages(target), rememberKiloImageModels(models)
  and useTargetSupportsImages(targetId).
- LocalModelProvider.supportsImages(modelId). Every on-device provider
  returns false.
- Kilo models read image support from the gateway catalog
  (architecture.input_modalities). Custom backend models get an images
  flag with a toggle. A stored model without the flag reads as text-only.

The router replaces image parts with a short text marker for a target
that does not read images, so a model switch after an image does not
fail at the provider. The unanswered question keeps its images for
Retry, and the transcript shows them.

* feat(mobile): read images with downloaded vision models

A downloaded GGUF model can now read images when it has a vision
projector (mmproj). The catalog adds SmolVLM 500M Instruct with its
projector, both pinned to one Hugging Face revision with size, SHA-256
and licence.

- Downloads: a vision model downloads the model file, then the
  projector, as one download with one progress bar and one storage
  check. Cancel, failure and delete remove both files.
- Records: a record has a vision flag. A record saved before this
  change reads as text-only.
- Inspection: a vision model loads in full with its projector, and the
  download is accepted only when llama.cpp reports vision support.
- Runtime: the context loads the projector after init with ctx_shift
  off. supportsImages is true only for a verified vision record.
- Template: a user turn with an image goes to llama.rn as content parts
  with a base64 data URL. llama.rn decodes it, so no image file is
  written. A model without a projector does not get images.

Direct-link models stay text-only.

* fix(mobile): keep the catalog file type private

* fix(mobile): show no upload progress on a local Quick Chat image

A local image never uploads, so its chip starts with no progress value
and the strip does not read "0%".

* fix(mobile): release the native image on every encode path

The encoder now releases the rendered image after a failed read or a
failed save, not only after a successful one. Tests pin the release on
each path, and that the images flag survives a backend save.

* test(mobile): pin the native image release and the backend image flag

Tests: the encoder releases each rendered image after a read, a resize, a
failed save and a too-large result; a backend save keeps the image flag.

* fix(mobile): a local Quick Chat image is not an upload

A locally delivered image chip had status 'pending', which the strip
treats as an upload in flight: a permanent spinner and an "Uploading…"
announcement for an image that never leaves the device. Local chips now
carry a 'local' status that starts no upload, shows no progress and
announces the file size instead.

* fix(mobile): drop the duplicate llama.rn proguard rule

The merge of main added a second extraProguardRules next to main's combined
rule, which app.config.ts rejects as a duplicate key. Keep main's combined rule.

* ci(trufflehog): exclude the synthetic-credential test fixture

security.test.ts builds a credential URL from the literal
'user:private-key@provider.example'. The commit that added it stays in this
branch's history, so trufflehog reports it on every base..head scan even though
a later commit replaced it with a constructed value. Exclude the fixture path.

* fix(db): drop the stale duplicate 0263_drop_quick_chat migration

main lands the drop as 0274_drop_quick_chat.sql (registered in the journal). The
same drop sits in this branch's lineage as 0263_drop_quick_chat.sql, unregistered
and colliding with 0263_spend_alerts_rollup_covering_index. The merge kept it
because main never had that path. Remove it; the tree now matches main.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
iscekic added a commit that referenced this pull request Oct 11, 2026
* feat(harness-sdk): the kilo harness SDK

#5878

* feat(mobile): rebuild the chat tab on the harness SDK

#5895

* chore(kilo-mcp): refresh catalog.json from CI

* feat(mobile): connect chat to the Kilo MCP server with the signed-in session (#6420)

#6420

* chore: publish the verified app-agent settings tools + remote MCP stack as one PR (level 1)

* fix(mobile): preserve settings and remote MCP state across cold loads

* fix(mobile): satisfy async and control-flow lint rules

* fix(harness-sdk): keep the library's abort link until the operation ends

fetch resolves when the headers arrive, so the per-request unlink let close()
leave an open stream running until the deadline timer fired. The deadline now
holds each joined signal once and removes it at stop.

* fix(harness-sdk): bound the credential by the operation's deadline

The token was asked before the deadline existed, so a slow refresh could hold
a chat open past the discovery bound. The deadline now comes first and the
credential races it.

* fix(harness-sdk): give a call a deadline above the time it is waited on inline

A remote tool is waited on for 15 s before the session backgrounds it, and the
call's own default deadline was also 15 s, so a backgrounded call failed at
that moment instead of answering. A call now defaults to 60 s; discovery keeps
15 s.

* fix(mobile): publish the Kilo tools answer that a cache hit serves

The registry builds a chat from the snapshot, not from the return value. A
cache hit returned the ready answer while another scope's discovery had
published connecting over it, so the chat opened on the base tools.

* fix(mobile): keep each Kilo tools discovery on its caller's deadline

A caller that joined a running discovery took the first caller's deadline, so
an open that met a Retry waited fifteen seconds and a Retry that met an open
was cut to four. A caller now joins only a discovery with its own deadline,
and only the latest discovery is published.

* fix(mobile): lock the Kilo tools switch when there is nothing to use

In the unavailable state the switch read off while the chat's setting was on,
so a tap wrote the value already stored and the switch snapped back. The view
now says whether the switch can move, and the sheet disables it when not.

* fix(deps): rebuild the lockfile on main's pins

The merge rebuilt the lockfile from the branch side, so pnpm resolved again
every package main had added since. That moved @peculiar/asn1-* to 2.9.5 and
broke the passkey verification tests in apps/web. The lockfile is now main's,
with only the harness SDK's own entries added.

* fix(mobile): pass the segments to the toaster's tab bar check

Main added a shouldHideTabBar call in app-root-providers.tsx with the old
one-argument form. This branch reads the chat conversation from the segments,
so the call now passes them too.

* test(mobile): read the hoisted query client mock in the account switch test

Main's lifecycle file already holds queryClientMock at module scope. The
branch's test declared it again from a dynamic import, which lint refuses as a
shadow and as an unbound method.

* fix(mobile): describe the spend alerts notification setting

Main added the spendAlerts notification category. The agent settings catalog
maps every category to a description, so typecheck failed on the missing one.

* fix(mobile): use the shared MCP servers copy in the chat sheet

Main added profiles.mcp.title and profiles.mcp.add with the same English copy
as modelChat.mcp.serversSection and modelChat.mcp.addServer, and the catalog
check refuses two keys for one copy. The chat section now reads the profile
keys, and the two chat keys are removed from every catalog.

* test(mobile): load the real config module in the MCP URL test

vitest.setup.ts now stubs @/lib/config for every pure suite, so the plain
dynamic import in config.test.ts returned the stub and KILO_MCP_URL read as
undefined. The test now loads the module through vi.importActual.

* test(mobile): allow the remote MCP id slug in the case guard

slugFor in remote-mcp-store.ts lowercases a server name to build the callable
tool-name id. The id is never shown, so the file joins the non-display list.

* feat(mobile): support configurable Quick Chat backends

* test(harness): construct synthetic credential URL explicitly

* fix(mobile): address Quick Chat backend review findings

* fix(mobile): reopen a failed chat from Retry

* test(mobile): allow protocol-identifier case normalization in backend files

* fix(mobile): wait for stored backends before the send check

* feat(mobile): add on-device Apple Foundation Models to Quick Chat

* feat(mobile): add on-device Gemini Nano to Quick Chat on Android

* fix(mobile): reference the Retry label in the on-device background copy

* feat(mobile): add llama.rn GGUF models to Quick Chat

* style(mobile): format the on-device GGUF files

* test(mobile): allow the GGUF file-extension comparison in the case guard

* fix(mobile): keep unreadable model entries from deleting downloads

* test(mobile): split the model-index policy tests into their own suite

* fix(mobile): never rewrite a model list holding an unreadable entry

* fix(mobile): keep any list that did not parse in full from being rewritten

* fix(mobile): read a GGUF result that carries nulls or only text

* fix(mobile): treat a null parsed content as absent

* test(mobile): pin the null-heavy llama.rn result at the client level

* feat(mobile): give the Apple on-device model the chat's tools

Foundation Models calls harness tools through dynamic tools. The model's
calls go to JavaScript as harness toolCall parts with the transcript ids.
The generation waits while the harness runs them, and the next request
resumes it with the results. Later requests rebuild the transcript with
the tool calls and their outputs.

JavaScript reduces each JSON Schema to what DynamicGenerationSchema can
express. A tool that does not fit is left out and logged. Gemini Nano
stays text-only because the ML Kit Prompt API has no function calling.

* fix(mobile): keep native schema and message types module-private

* fix(mobile): count tokens for a request that ends on tool outputs

A resumed tool round sends a request whose last entry is a tool output.
The Apple module refused to count it, so usage fell back to the estimate.
Only generation now needs a user prompt last.

* fix(mobile): drop the duplicate llama.rn proguard rule

The merge of main added a second extraProguardRules next to main's combined
rule, which app.config.ts rejects as a duplicate key. Keep main's combined rule.

* fix(kilo-app): build the harness SDK before the EAS bundle

The release EAS build bundles apps/mobile with Metro, which resolves
@kilocode/harness-sdk through its package.json exports to dist/index.js. The
workspace package is not built on EAS, so the Android and iOS bundles fail with
'none of these files exist' and the release never submits.

Add the eas-build-post-install hook the EAS build runs after install. It builds
the SDK, so Metro finds dist before it bundles. Verified with
expo export:embed --eager --platform android: it fails without dist and
succeeds after the hook builds it.

* ci(trufflehog): exclude the synthetic-credential test fixture

security.test.ts builds a credential URL from the literal
'user:private-key@provider.example'. The commit that added it stays in this
branch's history, so trufflehog reports it on every base..head scan even though
a later commit replaced it with a constructed value. Exclude the fixture path.

* fix(db): drop the stale duplicate 0263_drop_quick_chat migration

main lands the drop as 0274_drop_quick_chat.sql (registered in the journal). The
same drop sits in this branch's lineage as 0263_drop_quick_chat.sql, unregistered
and colliding with 0263_spend_alerts_rollup_covering_index. The merge kept it
because main never had that path. Remove it; the tree now matches main.

* fix(mobile): keep NativeImage module-private

knip flags the export as unused: only native-request.ts builds an image part.
Drop the export keyword.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-by-human the merge bot routed this PR to a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants