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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
bun run --cwd packages/opencode typecheck
cd packages/opencode
for test_file in test/installation/*.test.ts test/cli/upgrade-channel.test.ts test/cli/tui/shared-server.test.ts test/cli/tui/thread.test.ts; do
bash -c 'export MENDCODE_DB="${TMPDIR:-/tmp}/opencode-test-data-$$/share/release.db"; exec bun test --timeout 30000 "$@"' bash "$test_file"
bash -c 'export MENDCODE_DB=":memory:"; exec bun test --timeout 30000 "$@"' bash "$test_file"
done

- name: Upload release artifacts to workflow
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 0.1.44-beta.7 - 2026-09-10

### Added

- Add passive prompt-cache controls by project, session, provider, and exact
model through `mendcode cache status|enable|disable` and the `cache` config
block, without enabling keepalive or remote-cache deletion.
- Add conservative smart-mode adapters, cache lineage fingerprints, and
provider cache observations that keep unsupported bindings unknown.

### Fixed

- Keep ChatGPT OAuth Responses Lite transport affinity separate from managed
cache keys, rotate affinity when instructions change or disappear, and honor
explicit cache disablement without forwarding MendCode's internal marker.
- Remove generated cache keys and provider annotations when the effective
policy is `off`.

### Tests

- Add configuration, adapter, lineage, provider-transform, and ChatGPT OAuth
regressions for cache scope resolution, safe fallback, and disablement.

## 0.1.44-beta.6 - 2026-09-09

### Fixed
Expand Down
123 changes: 123 additions & 0 deletions docs/cli-setup-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,129 @@ Common MendCode config paths:
- `.mendcode/tsm/state.json`: optional TSM state.
- `.mendcode/worktree/state.json`: managed/adopted worktree registry.

## Prompt Cache Controls

MendCode exposes passive prompt-cache controls through the `cache` block in the
project or global MendCode config. The controls affect cache keys and provider
cache annotations that MendCode sends with a request; they never delete a
provider's remote cache and they never schedule refresh or keepalive requests.

The public CLI is:

```bash
mendcode cache status [--format text|json|--json]
mendcode cache enable [--global] [--provider <id>] [--model <id>] [--session <id>] [--project-path <path>]
mendcode cache disable [--global] [--provider <id>] [--model <id>] [--session <id>] [--project-path <path>]
```

`on` is an alias for `enable` and `off` is an alias for `disable`. Without
`--global`, mutation commands update the active project's config. With
`--global`, they update the global config. `--project-path` is only valid with
`--global` and writes an exact normalized project-path override. A model target
requires `--provider`; provider, model, session, and project targets cannot be
combined.

### Configuration

The `cache` block is optional. If it is absent, MendCode preserves existing
provider behavior (`legacy` effective mode).

```jsonc
{
"cache": {
"mode": "smart",
"projects": {
"/Users/me/src/example": { "mode": "smart" }
},
"sessions": {
"mode": "selected",
"include": ["ses_allowed"],
"exclude": ["ses_blocked"]
},
"providers": {
"openai": {
"mode": "smart",
"models": ["openai/gpt-6-astra"],
"exclude_models": ["openai/legacy-model"]
},
"openrouter": {
"models": ["openai/gpt-5.6-mini"]
}
}
}
}
```

Supported values and scopes:

- `mode`: `off` suppresses MendCode cache controls; `smart` enables only the
verified passive-key paths described below. Omit it to keep legacy behavior.
- `projects`: exact normalized project paths mapped to `{ "mode": "off" }` or
`{ "mode": "smart" }`. Use absolute paths for portable global config.
- `sessions.mode`: `all`, `selected`, or `none`. `include` selects session IDs
and `exclude` blocks session IDs. An excluded session wins over inclusion.
- `providers`: provider IDs mapped to an optional `mode`, `models` allowlist,
and `exclude_models` list. Model matching accepts the configured runtime ID,
API model ID, and their final path component.

Resolution is deterministic. Disabling rules win in this order: global
`mode: off`, project `off`, session `none`/exclusion/not-selected, provider
`off`, then model exclusion or allowlist rejection. Enabling then resolves from
the global mode, project mode, provider selection, or explicitly included
session; otherwise the effective mode is `legacy`.

### Smart-mode boundaries

`smart` is deliberately conservative. A managed passive cache key is enabled
only for a binding that has all of the following verified locally:

- API-key authentication, not ChatGPT subscription OAuth;
- a non-Responses-Lite transport;
- OpenAI's `@ai-sdk/openai` SDK at `api.openai.com`, or the OpenRouter SDK/
compatible route at `openrouter.ai`;
- the exact provider, model, endpoint, SDK, authentication, and transport match.

Other providers, custom endpoints, OAuth routes, unknown models, and incomplete
bindings do not receive a newly inferred managed key. Existing provider-specific
legacy annotations remain governed by their existing transform path unless the
policy is explicitly `off`; MendCode does not infer TTL, cache warmth, hit rate,
price savings, or provider support from a model name alone.

`off` removes recognized cache fields from request options and message/provider
annotations, including `promptCacheKey`, `prompt_cache_key`, `cacheControl`,
`cachePoint`, `cache_control`, `copilot_cache_control`, and gateway
`caching: "auto"`. For ChatGPT OAuth, the internal disable marker is consumed
before the request is forwarded upstream. This is local request shaping, not
remote-cache delete access.

### Scope examples

```bash
# Inspect the effective policy for the current project.
mendcode cache status --format json

# Enable conservative caching for the current project's OpenAI provider.
mendcode cache enable --provider openai

# Allow one exact model and disable another model on the same provider.
mendcode cache enable --provider openai --model openai/gpt-6-astra
mendcode cache disable --provider openai --model openai/legacy-model

# Limit caching to one session, or disable it for a session.
mendcode cache enable --session ses_allowed
mendcode cache disable --session ses_blocked

# Manage an exact project override from the global config.
mendcode cache enable --global --project-path /Users/me/src/example
mendcode cache disable --global --project-path /Users/me/src/example
```

`cache status` reports the effective mode, the current project, the configured
block, and `activeKeeper: false`. Cache Keeper, scheduled refresh, automatic
warm-up, and provider canaries are not part of this control surface. Missing
provider cache metrics remain unknown rather than being converted to a zero or
used to claim a cache hit.

## Prompt Draft Undo and Redo

The TUI keeps a temporary in-memory edit history for the active prompt:
Expand Down
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The pitch is not “another chat box.” The pitch is a configurable coding term
| Loop Workflows | Durable, monitorable loop sessions with `/loop` creation, `/loops` supervision, Agent View roots, contract-aware report-only mode, and a per-project OS background service. | [Loop Workflows](loop-workflows.md) |
| Automation runtime | Let another local agent create, continue, inspect, wait for, stream, and cancel real MendCode sessions through the same runtime, with versioned JSON output and shared model selection. | [Automation runtime](automation-runtime.md) |
| Usage Insights | Local activity dashboard for tokens, sessions, AI time, words, tools, agents, models, changed files, daily activity, selected-day detail, cache mix, and optional weather. | [Usage Insights](usage-insights.md) |
| Passive prompt-cache controls | Configure legacy/off/smart cache behavior by project, session, provider, and exact model without enabling keepalive or remote-cache deletion. | [CLI, setup, and configuration](cli-setup-configuration.md#prompt-cache-controls) |
| Approval-gated memory | Memory can retrieve context without silently turning every session into permanent state. Generated memories become reviewable proposals first. | [CLI, setup, and configuration](cli-setup-configuration.md#permissions-and-memory) |
| Memory Center, graph, and Dream | Route-level memory workspace with saved/pending memories, categories, policy controls, Dream logs, and constrained memory side chat. | [Memory Center](memory-center.md) |
| Smart permissions | Choose `approval`, `smart`, or `full_access`. Smart mode auto-approves bounded read-only shell work and can route risky decisions through a configured `permissionReviewer` role. | [CLI, setup, and configuration](cli-setup-configuration.md#permissions-and-memory) |
Expand Down
129 changes: 129 additions & 0 deletions src/mendcode/packages/opencode/src/cli/cmd/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Argv } from "yargs"
import { Effect } from "effect"
import { Config } from "@/config/config"
import { ConfigCache, type CacheMutation } from "@/config/cache"
import { Instance } from "@/project/instance"
import { cmd } from "./cmd"
import { effectCmd, fail } from "../effect-cmd"
import { UI } from "../ui"

type CacheArgs = {
global?: boolean
provider?: string
model?: string
session?: string
projectPath?: string
}

function cacheOptions(yargs: Argv) {
return yargs
.option("global", {
type: "boolean",
default: false,
describe: "write the global config instead of the current project config",
})
.option("provider", {
type: "string",
describe: "provider ID to target",
})
.option("model", {
type: "string",
describe: "exact model ID to target; requires --provider",
})
.option("session", {
type: "string",
describe: "session ID to include or exclude",
})
.option("project-path", {
type: "string",
describe: "absolute or relative project path to target in global config",
})
}

function mutationFromArgs(args: CacheArgs, action: CacheMutation["action"]): CacheMutation {
if (args.projectPath && !args.global) throw new Error("--project-path requires --global")
return {
action,
providerID: args.provider,
modelID: args.model,
sessionID: args.session,
projectPath: args.projectPath,
}
}

export const CacheStatusCommand = effectCmd({
command: "status",
describe: "show prompt-cache policy and configured scopes",
builder: (yargs) =>
yargs
.option("format", {
type: "string",
choices: ["text", "json"],
default: "text",
describe: "output format",
})
.option("json", {
type: "boolean",
default: false,
describe: "output JSON",
}),
handler: Effect.fn("Cli.cache.status")(function* (args: { format?: string; json?: boolean }) {
const config = yield* Config.Service
const current = yield* config.get()
const status = {
project: Instance.directory,
mode: ConfigCache.selectCacheConfig({
config: current.cache,
projectScope: Instance.directory,
}).mode,
configured: current.cache ?? null,
activeKeeper: false,
note: "MendCode only uses passive provider cache controls; no keepalive requests are scheduled.",
}
if (args.json || args.format === "json") {
UI.println(JSON.stringify(status, null, 2))
return
}
UI.println(`Prompt cache: ${status.mode}`)
UI.println(`Project: ${status.project}`)
UI.println("Active keeper: disabled")
if (!current.cache) UI.println("Configuration: legacy provider behavior (no cache override)")
else UI.println(`Configuration: ${JSON.stringify(current.cache)}`)
}),
})

const CacheMutationCommand = (action: CacheMutation["action"]) =>
effectCmd({
command: action,
aliases: action === "enable" ? ["on"] : ["off"],
describe: `${action === "enable" ? "enable" : "disable"} passive prompt caching for a scope`,
instance: (args: CacheArgs) => !args.global,
builder: cacheOptions,
handler: Effect.fn(`Cli.cache.${action}`)(function* (args: CacheArgs) {
const config = yield* Config.Service
const current = args.global ? yield* config.getGlobal() : yield* config.get()
let next: ConfigCache.Info
try {
next = ConfigCache.updateCacheConfig(current.cache, mutationFromArgs(args, action))
} catch (error) {
return yield* fail(error instanceof Error ? error.message : String(error))
}

if (args.global) yield* config.updateGlobal({ cache: next })
else yield* config.update({ cache: next })

UI.println(`${action === "enable" ? "Enabled" : "Disabled"} prompt cache policy.`)
UI.println(JSON.stringify(next, null, 2))
}),
})

export const CacheEnableCommand = CacheMutationCommand("enable")
export const CacheDisableCommand = CacheMutationCommand("disable")

export const CacheCommand = cmd({
command: "cache",
describe: "control passive provider prompt caching",
builder: (yargs) =>
yargs.command(CacheStatusCommand).command(CacheEnableCommand).command(CacheDisableCommand).demandCommand(),
async handler() {},
})
Loading
Loading