Merge branch 'main' into feat/mcp-apps-experimental

This commit is contained in:
Rômulo Rosa Furtado
2026-08-20 16:13:03 +02:00
committed by GitHub
313 changed files with 6263 additions and 1268 deletions
+56
View File
@@ -1,5 +1,61 @@
# @kilocode/cli
## 7.4.23
### Minor Changes
- [#13137](https://github.com/Kilo-Org/kilocode/pull/13137) [`90a93a7`](https://github.com/Kilo-Org/kilocode/commit/90a93a7aa25950d5894fa67f6e4e6545ef55017c) - Add `kilo pr link <url>`, `kilo pr unlink`, and `kilo pr status` to link the current worktree to a pull request. The checkout command moves to `kilo pr checkout <number>`; `kilo pr <number>` no longer checks out a PR.
### Patch Changes
- [#13206](https://github.com/Kilo-Org/kilocode/pull/13206) [`e13c6d3`](https://github.com/Kilo-Org/kilocode/commit/e13c6d3ee93d123c6fc187592fc28324c8840fc2) - Fix Agent Manager ignoring requests to start new sessions on OpenAI Responses API models, where a start request was answered with a list of existing sessions instead of creating the worktree or session.
- [#13124](https://github.com/Kilo-Org/kilocode/pull/13124) [`d4f3a3a`](https://github.com/Kilo-Org/kilocode/commit/d4f3a3a9e63a3954214887563dc3816ea179858f) - Stop broad permission rules from letting Ask and Plan modes change your workspace. Catch-all approvals, the "Allow everything" toggle, and the `<command> *` rules that "Always allow" persists no longer grant these modes shell commands, subagents, notebook edits or other mutating tools, and MCP tools go back to prompting. To opt a single mode in, set `agent.ask.permission` or `agent.plan.permission` instead of a top-level `permission` rule.
- [#13121](https://github.com/Kilo-Org/kilocode/pull/13121) [`e2966ab`](https://github.com/Kilo-Org/kilocode/commit/e2966abcba4906383d13b385d61ecd50016e4d7d) - Keep the selected agent after switching from Ask to Code, and remind the model that previous Ask-mode restrictions no longer apply.
- [#13224](https://github.com/Kilo-Org/kilocode/pull/13224) [`b1755f9`](https://github.com/Kilo-Org/kilocode/commit/b1755f91848b083533e675ba38750063862344d4) - Keep one-time waits in the blocking shell tool instead of tracking them as background processes.
- [#13209](https://github.com/Kilo-Org/kilocode/pull/13209) [`ff16bc2`](https://github.com/Kilo-Org/kilocode/commit/ff16bc2c9c25d4f11af5303cd79267546456cb1e) - Keep Agent Manager terminals and nested Kilo sessions alive across configuration reloads and location idle eviction, while cleaning them up on explicit close, worktree deletion, and server shutdown.
- [#13115](https://github.com/Kilo-Org/kilocode/pull/13115) [`d9f0eff`](https://github.com/Kilo-Org/kilocode/commit/d9f0eff30634410738c82355641b4e6353c135bb) - Start Kilo with a persistent fallback when the default runtime state directory is not writable.
- [#13210](https://github.com/Kilo-Org/kilocode/pull/13210) [`8d717a0`](https://github.com/Kilo-Org/kilocode/commit/8d717a05d322a2382af05d8265ec2354c893a674) - Remove the duplicate skill catalog from the model-facing skill tool description.
- [#13183](https://github.com/Kilo-Org/kilocode/pull/13183) [`017410b`](https://github.com/Kilo-Org/kilocode/commit/017410bf6fbfdaa1e3a050f00f7e0cd3dd5371ad) - Accept JWT share tokens when importing a session from a Kilo share URL.
- [#13165](https://github.com/Kilo-Org/kilocode/pull/13165) [`5e6e93a`](https://github.com/Kilo-Org/kilocode/commit/5e6e93aa19e984d8699d1a3199b49f8e726e899c) - Use the full Codex context window for GPT-5.6 models authenticated through ChatGPT OAuth.
- [#13249](https://github.com/Kilo-Org/kilocode/pull/13249) [`3de7df2`](https://github.com/Kilo-Org/kilocode/commit/3de7df279bceab97fc1fd75eb0a9e45e735c472d) - Require the todo tool to update multi-step lists after each completed task.
- [#13122](https://github.com/Kilo-Org/kilocode/pull/13122) [`c34a2a3`](https://github.com/Kilo-Org/kilocode/commit/c34a2a3a42f9f07d6ae2643ec655720bdb84c820) - Stop memory auto-save from failing on OpenAI-compatible providers that stream by default.
- [#13195](https://github.com/Kilo-Org/kilocode/pull/13195) [`f54c215`](https://github.com/Kilo-Org/kilocode/commit/f54c215e6ab2f22e055790ccc4a2d122992dba48) Thanks [@quanzhuo](https://github.com/quanzhuo)! - Persist disabling snapshots from the slow-repo prompt across restarts.
- [#13112](https://github.com/Kilo-Org/kilocode/pull/13112) [`2eb6300`](https://github.com/Kilo-Org/kilocode/commit/2eb630053f5bb822eb6a2b9e830afecfd0f6163c) - Switch to the code model when starting implementation after a planning session.
- [#13225](https://github.com/Kilo-Org/kilocode/pull/13225) [`da10638`](https://github.com/Kilo-Org/kilocode/commit/da1063865480bb7cd2aeeb8c18a949a805bd4872) - Remove the experimental agent requirements check and its configuration flag.
- [#13214](https://github.com/Kilo-Org/kilocode/pull/13214) [`e1bcb32`](https://github.com/Kilo-Org/kilocode/commit/e1bcb320d94efe6ed8abf7c33fe6475a67a2e1e7) - Remove the experimental task-aware tool-output pruning feature and its related settings and indicators.
- [#13199](https://github.com/Kilo-Org/kilocode/pull/13199) [`6131ed2`](https://github.com/Kilo-Org/kilocode/commit/6131ed269f37ae8e258c1b91929c3170a4cf2767) - Keep `kilo upgrade` on the Kilo CLI release channel when GitHub's latest release is a JetBrains release.
- [#13178](https://github.com/Kilo-Org/kilocode/pull/13178) [`86af8dd`](https://github.com/Kilo-Org/kilocode/commit/86af8dd7c700fcb6229f28471126b5e7b0f6f654) - Prompt for explicit, one-shot approval before mutating Git commands run outside the sandbox.
- [#13103](https://github.com/Kilo-Org/kilocode/pull/13103) [`591772d`](https://github.com/Kilo-Org/kilocode/commit/591772d92875762460636b42233ad1ad552e8596) - Use provider model catalogs instead of hardcoded model-name heuristics for reasoning variants.
- [#13170](https://github.com/Kilo-Org/kilocode/pull/13170) [`3acb1ec`](https://github.com/Kilo-Org/kilocode/commit/3acb1ec38693e7f75bb38e832a23ace097c7440a) - Keep recently used Kilo Gateway models visible in the TUI picker, and find them when filtering by kilo.
- [#13247](https://github.com/Kilo-Org/kilocode/pull/13247) [`0d5d334`](https://github.com/Kilo-Org/kilocode/commit/0d5d334480bc2093a12b27a34b03cac88cf33422) - Fix TUI sessions where new turns stopped appearing until the session was reopened
- Updated dependencies [[`f39e163`](https://github.com/Kilo-Org/kilocode/commit/f39e1631855859222966350bd5fae373b9877297)]:
- @kilocode/kilo-gateway@8.0.0
- @opencode-ai/server@7.4.23
- @opencode-ai/tui@7.4.23
- @opencode-ai/ui@7.4.23
- @kilocode/kilo-indexing@7.4.23
- @kilocode/kilo-telemetry@7.4.23
## 7.4.22
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.4.22",
"version": "7.4.23",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -1,6 +1,4 @@
import type { AlibabaProviderOptions } from "@ai-sdk/alibaba"
import type { AnthropicProviderOptions } from "@ai-sdk/anthropic"
import type { MistralLanguageModelOptions } from "@ai-sdk/mistral"
import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"
import type { OpenAICompatibleProviderOptions } from "@ai-sdk/openai-compatible"
import type { OpenRouterProviderOptions } from "@openrouter/ai-sdk-provider"
@@ -27,11 +25,5 @@ export function kiloProviderOptions(options: { [x: string]: any }) {
openrouter.reasoning && "effort" in openrouter.reasoning ? openrouter.reasoning?.effort : undefined,
textVerbosity: openrouter.verbosity,
} satisfies OpenAICompatibleProviderOptions
result.alibaba = {
enableThinking: openrouter.reasoning?.enabled,
} satisfies AlibabaProviderOptions
result.mistral = {
reasoningEffort: openrouter.reasoning?.enabled ? "high" : undefined,
} satisfies MistralLanguageModelOptions
return result
}
@@ -205,9 +205,7 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
options,
async getModel(sdk: KiloProvider, modelID: string) {
const provider = input.models[modelID]?.ai_sdk_provider
if (provider === "alibaba") return sdk.alibaba(modelID)
if (provider === "anthropic") return sdk.anthropic(modelID)
if (provider === "mistral") return sdk.mistral(modelID)
if (provider === "openai") return sdk.openai(modelID)
if (provider === "openai-compatible") return sdk.openaiCompatible(modelID)
return sdk.languageModel(modelID)
@@ -451,6 +451,7 @@ export namespace KiloSessionPrompt {
info,
"Use the chosen plan path as the main plan file. Do not write or edit other files unless the user explicitly asks and your permissions allow it.",
"Project/user instructions about plan location (for example plans/ or .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.",
"In the visible final response, cite the saved plan path as an inline code span so the client can open it as a document. Cite other user-facing files you create the same way instead of pasting the full file into chat.",
supportsPlanFollowup()
? "When the plan is implementation-ready, write the main plan file and call plan_exit. Do not ask the user to choose between finalizing and refining in chat; the client follow-up after plan_exit asks whether to implement the saved plan or keep refining."
: 'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.',
+39 -26
View File
@@ -1,10 +1,12 @@
import { InstanceState } from "@/effect/instance-state"
import { registerDisposer } from "@/effect/instance-registry"
import type { InstanceContext } from "@/project/instance-context"
import * as Log from "@opencode-ai/core/util/log"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Cause, Context, Effect, Layer, Scope } from "effect"
import { Cause, Context, Effect, Exit, Layer, Scope } from "effect"
const log = Log.create({ service: "kilocode-watcher" })
@@ -17,13 +19,8 @@ export namespace KilocodeWatcher {
// Embedded editor clients (VS Code, JetBrains) have their own file watching
// and git integration and do not consume the CLI's vcs.branch.updated event,
// so they must not eagerly warm the location stack — that starts a native
// @parcel/watcher subscription per instance that lives for the whole session.
// On macOS FSEvents watches the entire subtree recursively (the ignore list
// is only a userspace filter), so an always-on, consumer-less watcher on a
// churny workspace burns CPU and leaks native memory while idle. The
// standalone CLI/TUI stays eager because its sidebar branch label is the only
// consumer and no request-driven route would otherwise build the stack.
// so they must not eagerly warm the location stack. The standalone CLI/TUI
// keeps this subscription for live branch-label updates.
export function eager(client = Flag.KILO_CLIENT) {
return client !== "vscode" && client !== "jetbrains"
}
@@ -33,31 +30,47 @@ export namespace KilocodeWatcher {
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
const scope = yield* Scope.Scope
const active = new Map<string, Scope.Closeable>()
const ref = (directory: string) => Location.Ref.make({ directory: AbsolutePath.make(directory) })
const state = yield* InstanceState.make(
Effect.fn("KilocodeWatcher.state")(function* (ctx) {
if (ctx.project.vcs !== "git") return
// Warm the v2 location stack for this instance and hold it for the
// instance lifetime. Its Watcher subscribes to .git so Vcs sees HEAD
// changes and publishes vcs.branch.updated in the CLI, where no v2
// route would otherwise build the stack. The ref must be built the
// same way the file/pty handlers build theirs (Location.Ref.make) so
// the LayerMap shares a single build per directory.
const ref = Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })
yield* locations.contextEffect(ref)
// Tear the stack down with the instance instead of letting it idle
// in the LayerMap; same pattern as the pty handlers' disposer.
yield* Effect.addFinalizer(() => locations.invalidate(ref).pipe(Effect.ignore))
}),
const off = registerDisposer((directory) =>
Effect.runPromise(
Effect.gen(function* () {
const child = active.get(directory)
if (child) {
active.delete(directory)
yield* Scope.close(child, Exit.void)
}
yield* locations.invalidate(ref(directory))
}).pipe(Effect.ignore),
),
)
yield* Effect.addFinalizer(() => Effect.sync(off))
yield* Effect.addFinalizer(() =>
Effect.forEach(active.values(), (child) => Scope.close(child, Exit.void), { discard: true }).pipe(
Effect.andThen(Effect.sync(() => active.clear())),
),
)
const warm = (ctx: InstanceContext, child: Scope.Closeable) =>
Scope.provide(child)(locations.contextEffect(ref(ctx.directory)))
return Service.of({
init: Effect.fn("KilocodeWatcher.init")(function* () {
yield* InstanceState.get(state).pipe(
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git" || active.has(ctx.directory)) return
const child = yield* Scope.make()
active.set(ctx.directory, child)
yield* warm(ctx, child).pipe(
Effect.catchCause((cause) =>
Effect.sync(() => log.warn("instance watcher init failed", { err: Cause.squash(cause) })),
Effect.gen(function* () {
if (active.get(ctx.directory) === child) active.delete(ctx.directory)
yield* Scope.close(child, Exit.void).pipe(Effect.ignore)
yield* Effect.sync(() => log.warn("instance watcher init failed", { err: Cause.squash(cause) }))
}),
),
Effect.forkIn(scope),
Effect.forkIn(scope, { startImmediately: true }),
)
}),
})
+3
View File
@@ -23,6 +23,9 @@ Skip when:
## Rules
- Update status in real time; don't batch completions
- After creating a list, call this tool before starting the first item.
- After completing each item, call this tool before starting the next item, with the completed item marked `completed` and the next item marked `in_progress`.
- Do not complete multiple items or continue through multiple steps without updating the list between them.
- Mark `completed` only after the required work is actually done, including any required verification. Never based on intent.
- Keep exactly one `in_progress` while work remains
- If blocked or partial, keep it `in_progress` and add a follow-up todo describing the blocker
@@ -1,14 +1,20 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { Context, Deferred, Effect, Fiber, Layer, LayerMap } from "effect"
import * as TestConsole from "effect/testing/TestConsole"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { InstanceRef } from "../../src/effect/instance-ref"
import { disposeInstance } from "../../src/effect/instance-registry"
import { Git } from "../../src/git"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { KilocodeWatcher } from "../../src/kilocode/watcher"
import type { InstanceContext } from "../../src/project/instance-context"
import { tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, type LocationServices } from "@opencode-ai/core/location-services"
const layer = Layer.mergeAll(
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
@@ -44,47 +50,104 @@ describe("KilocodeWatcher.eager", () => {
})
})
live("instances publish branch updates after git switch", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const git = yield* Git.Service
const store = yield* InstanceStore.Service
const current = yield* git.branch(dir)
if (!current) return yield* Effect.die("missing initial branch")
live(
"instances publish branch updates after git switch",
() =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const git = yield* Git.Service
const store = yield* InstanceStore.Service
const current = yield* git.branch(dir)
if (!current) return yield* Effect.die("missing initial branch")
const branch = `watch-${Math.random().toString(36).slice(2)}`
const created = yield* git.run(["branch", branch], { cwd: dir })
expect(created.exitCode).toBe(0)
yield* store.load({ directory: dir })
const branch = `watch-${Math.random().toString(36).slice(2)}`
const created = yield* git.run(["branch", branch], { cwd: dir })
expect(created.exitCode).toBe(0)
yield* store.load({ directory: dir })
const pending = yield* Deferred.make<string | undefined>()
const handler = (event: GlobalEvent) => {
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
if (event.payload.properties.branch !== branch) return
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
}
GlobalBus.on("event", handler)
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
// The watcher exposes no readiness signal (its .git subscription is forked
// during instance warm-up), so keep generating HEAD churn in the background
// and synchronize on the event itself with the full test budget.
const churn = yield* Effect.gen(function* () {
while (true) {
yield* git.run(["switch", current], { cwd: dir })
yield* Effect.sleep("50 millis")
yield* git.run(["switch", branch], { cwd: dir })
yield* Effect.sleep("100 millis")
const pending = yield* Deferred.make<string | undefined>()
const handler = (event: GlobalEvent) => {
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
if (event.payload.properties.branch !== branch) return
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
}
}).pipe(Effect.forkScoped)
GlobalBus.on("event", handler)
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
const updated = yield* awaitWithTimeout(
Deferred.await(pending),
"timed out waiting for vcs.branch.updated",
"15 seconds",
)
yield* Fiber.interrupt(churn)
expect(updated).toBe(branch)
}),
// The watcher exposes no readiness signal (its .git subscription is forked
// during instance warm-up), so keep generating HEAD churn in the background
// and synchronize on the event itself with the full test budget.
const churn = yield* Effect.gen(function* () {
while (true) {
yield* git.run(["switch", current], { cwd: dir })
yield* Effect.sleep("50 millis")
yield* git.run(["switch", branch], { cwd: dir })
yield* Effect.sleep("100 millis")
}
}).pipe(Effect.forkScoped)
const updated = yield* awaitWithTimeout(
Deferred.await(pending),
"timed out waiting for vcs.branch.updated",
"15 seconds",
)
yield* Fiber.interrupt(churn)
expect(updated).toBe(branch)
}),
20_000,
)
test.serial(
"isolates location lifetimes between instances",
async () => {
await Effect.runPromise(
Effect.gen(function* () {
const one = yield* tmpdirScoped()
const two = yield* tmpdirScoped()
const warmed = new Map<string, number>()
const invalidated: string[] = []
const map = yield* LayerMap.make((ref: Location.Ref) =>
Layer.effectContext(
Effect.acquireRelease(
Effect.sync(() => {
warmed.set(ref.directory, (warmed.get(ref.directory) ?? 0) + 1)
return Context.empty() as Context.Context<LocationServices>
}),
() => Effect.sync(() => invalidated.push(ref.directory)),
),
),
)
const watcher = KilocodeWatcher.layer.pipe(Layer.provide(Layer.succeed(LocationServiceMap.Service, map)))
const services = yield* Layer.build(watcher)
const init = (directory: string) =>
KilocodeWatcher.Service.use((service) => service.init()).pipe(
Effect.provide(services),
Effect.provideService(InstanceRef, {
directory,
worktree: directory,
project: { vcs: "git" },
} as InstanceContext),
)
yield* init(one)
yield* init(one)
yield* init(two)
yield* Effect.yieldNow
expect(warmed).toEqual(
new Map([
[one, 1],
[two, 1],
]),
)
yield* Effect.promise(() => disposeInstance(one))
expect(invalidated).toEqual([one])
yield* Effect.promise(() => disposeInstance(two))
expect(invalidated).toEqual([one, two])
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(AppNodeBuilder.build(CrossSpawnSpawner.node), TestConsole.layer)),
),
)
},
20_000,
)
@@ -0,0 +1,10 @@
import { describe, expect, test } from "bun:test"
import DESCRIPTION_WRITE from "../../src/tool/todowrite.txt"
describe("todowrite description", () => {
test("requires an update between each task", () => {
expect(DESCRIPTION_WRITE).toContain("call this tool before starting the first item")
expect(DESCRIPTION_WRITE).toContain("After completing each item, call this tool before starting the next item")
expect(DESCRIPTION_WRITE).toContain("Do not complete multiple items or continue through multiple steps")
})
})
@@ -0,0 +1,79 @@
import { describe, expect } from "bun:test"
import { createKiloClient } from "@kilocode/sdk/v2"
import { Effect } from "effect"
import { cliIt } from "../lib/cli-process"
const todos = [
[
{ content: "Inspect files", status: "in_progress", priority: "high" },
{ content: "Implement fix", status: "pending", priority: "medium" },
{ content: "Run checks", status: "pending", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "in_progress", priority: "medium" },
{ content: "Run checks", status: "pending", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "in_progress", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "completed", priority: "medium" },
{ content: "Report results", status: "in_progress", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "completed", priority: "medium" },
{ content: "Report results", status: "completed", priority: "low" },
],
] as const
describe("todowrite end-to-end", () => {
cliIt.live(
"persists every sequential update through the real CLI session",
({ llm, opencode }) =>
Effect.gen(function* () {
const server = yield* opencode.serve()
const client = createKiloClient({ baseUrl: server.url })
const session = yield* Effect.promise(() =>
client.session.create({
permission: [{ permission: "*", action: "allow", pattern: "*" }],
}),
)
const sessionID = session.data?.id
if (!sessionID) throw new Error("test session was not created")
for (const list of todos) yield* llm.tool("todowrite", { todos: list })
yield* llm.text("done")
const result = yield* opencode.run("complete this four-step task", {
extraArgs: ["--attach", server.url, "--session", sessionID, "--auto"],
timeoutMs: 90_000,
})
opencode.expectExit(result, 0)
expect(JSON.stringify(yield* llm.inputs)).toContain(
"After completing each item, call this tool before starting the next item",
)
const saved = yield* Effect.promise(() => client.session.todo({ sessionID }))
const final = todos[todos.length - 1]?.map((todo) => ({ ...todo }))
expect(saved.data).toEqual(final)
const messages = yield* Effect.promise(() => client.session.messages({ sessionID }))
const calls =
messages.data?.flatMap((message) =>
message.parts.filter((part) => part.type === "tool" && part.tool === "todowrite"),
) ?? []
expect(calls).toHaveLength(todos.length)
}),
120_000,
)
})