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
5 changes: 4 additions & 1 deletion .github/workflows/runtime-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ jobs:
}
for test_file in \
test/installation/*.test.ts \
test/storage/compatibility.test.ts test/storage/writer-lease.test.ts \
test/storage/compatibility.test.ts test/storage/writer-lease.test.ts test/storage/sql-identifiers.test.ts \
test/mend/code-mode.test.ts test/session/context-profile.test.ts test/session/tool-discovery.test.ts \
test/mend/smart-context.test.ts test/mend/smart-service.test.ts test/mend/smart-approval.test.ts \
test/permission/next.test.ts test/tool/shell-analysis.test.ts \
test/cli/run-attach.test.ts test/cli/upgrade-channel.test.ts test/cli/upgrade-readonly.test.ts \
test/cli/tui/shared-server.test.ts test/cli/tui/thread.test.ts \
test/cli/tui/widgets-runtime.test.tsx test/cli/tui/widgets-tray.test.tsx \
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## 0.1.44-beta.3 - 2026-09-05

### Fixed

- Update Drizzle ORM and Kit to the fixed `1.0.0-beta.20` versions, including
correct SQL identifier escaping (GHSA-gpj5-g38j-94v9). No schema migration is generated.

### Added

- Backend-owned Smart Approval review, exact action grants, history and revocation,
with causal user authority and matching TUI, SDK and ACP controls.
- `mendcode context` for estimated context composition, provider-reported cache
usage and request timing. Missing provider metrics remain unavailable.
- Stable per-turn memory snapshots and bounded secondary-tool discovery, with a
configuration fallback to the complete permitted catalog.
- Opt-in Code Mode adapted from the pinned OpenCode v2 interpreter. Nested tools
retain host permissions and audit records while only the final result enters
model context. Code Mode remains disabled by default.
- Native macOS screenshot capture with crop/window targeting, image attachments,
bounded previews and limited navigation keys tied to a recent capture.

### Compatibility and validation limits

- This is an experimental beta. Native provider compaction is not enabled;
the portable summarizer remains active. No measured cache-saving claim is made.
- Native computer tools currently support macOS only. Pointer control, arbitrary
typing and browser DOM automation are not included. Interactive keyboard and
Smart Approval TUI acceptance remain pending; OS permissions are not bypassed.
- Code Mode is a confined language in a worker, not an OS sandbox or hard heap
quota. Broader resource and permission lifecycle validation remains pending.
- No database migration or automatic continuity activation is introduced.
Existing beta databases must not be forced back to the stable runtime.

## 0.1.44 stable - 2026-09-05

The stable hotfix is published separately from the beta runtime. It carries the
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Use this index by intent. **Use MendCode** explains the public workflow, **Featu
## Use MendCode

- [CLI, setup, and configuration](cli-setup-configuration.md): install/open commands, setup state, config paths, focus profiles, model roles, prompt modes, permissions, and memory.
- [Context, cache and computer tools](context-and-computer-tools.md): request profiles, tool discovery, experimental Code Mode, and native macOS captures.
- [Session history](session-history.md): browse, inspect, and resume previous sessions.
- [Automation runtime](automation-runtime.md): JSON envelopes, shared model selection, progress inspection, events, waiting, and cancellation.
- [Independent Workflows](workflows.md): one-shot plans, task sequencing, completion evidence, and operator controls.
Expand Down
103 changes: 103 additions & 0 deletions docs/context-and-computer-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Context, cache and computer tools

MendCode records a small profile for each completed model step. Inspect recent
profiles from the same workspace as the session:

```sh
mendcode context <sessionID>
mendcode context <sessionID> --limit 50 --json
```

The report separates estimated instruction, recalled memory, conversation,
tool-result, tool-schema and media tokens. These estimates are not provider
billing counts. Actual input/cache counts come from provider usage; missing
fields remain unavailable. Timing measures the request and first streamed text
or reasoning event. Fingerprints identify stable instruction/schema prefixes
without retaining their contents in the profile. Existing sessions gain profiles
only for new completed steps. The command reads a bounded recent-message page,
not a lifetime total.

Memory retrieval is stored as synthetic reference context at each user turn.
Repeated tool steps reuse that snapshot; the next user turn retrieves memory
again. Synthetic memory never grants permission or replaces the user's request.

## Tool discovery

Core tools retain their direct interfaces. Secondary tools are discovered with
`tool_search`; returned schemas enable direct calls in subsequent steps. Search
returns at most eight schemas and 24 KiB. Up to 64 secondary tools remain active
in retained history. Disabled tools stay unavailable. Schemas too large for the
search budget are omitted, not truncated into invalid definitions.

Set `experimental.tool_discovery` to `false` in the existing MendCode JSON
configuration to send the complete permitted catalog. Disabling `tool_search`
also restores the complete catalog so capabilities do not become inaccessible.
Cache savings depend on the provider and workload; no measured saving or parity
with another harness is claimed.

## Experimental Code Mode

Enable `experimental.code_mode: true` to expose `code`. It runs a confined
JavaScript subset in a worker. Use `tools.search({query: "..."})` to discover
signatures, call tools with `await tools.name(args)`, and return a concise value:

```js
const result = await tools.read({ filePath: "/workspace/report.txt" })
return { characters: result.output.length }
```

The host enforces 32 KiB source, 16 tool calls, a 30-second wall-clock deadline
and 24 KiB retained result/log output. Every nested call uses the existing tool
implementation, permissions and session audit record. Its intermediate result
stays out of model history; only the outer result enters it. Call image-producing
tools directly to view images. Cancellation waits for host tool cleanup.

There are no process, filesystem, network or import globals in the language.
Worker separation is not an OS sandbox or a hard heap quota. Code Mode remains
off by default pending broader resource, permission and cross-platform packaged-runtime testing.
The interpreter is adapted from OpenCode v2 at revision
`cd504dc66ac6620a662a0246f83cfc05f796f58a`; its MIT license and file provenance
are kept alongside the source. No dependency upgrade is required.

## Native macOS screenshots and navigation

`computer_capture` captures the main display, an explicit window ID, or an
integer screen-region crop. It returns a PNG attachment and an absolute file
path that `read` can reopen. Previews are resized to at most 1600 pixels;
coordinates in them must not be assumed to match desktop control coordinates.
Retained identical screenshots reuse the existing image reference. Capture
artifacts are limited to 64 per session and 8 MiB per inline image.

`computer_key` supports one navigation key against the foreground application
observed during a recent full-display capture on a single-display Mac. The
capture ID belongs to the same session, expires after 30 seconds and permits
one action. Foreground changes, stale captures and missing observations fail
closed. The tool never activates another application. Capture again afterward.
Window crops, multiple displays, pointer control and arbitrary typing do not
support this keyboard-control path.

Both tools use the normal MendCode permission boundary. macOS Screen Recording,
Automation and Accessibility permissions remain controlled by the OS; failures
are reported rather than bypassed. Native support on Windows/Linux and browser
DOM automation are not included. Connected MCP services remain discoverable.

## Compaction compatibility

The existing portable summarizer remains active. The installed OpenAI SDK can
replay encrypted reasoning but does not expose native `/responses/compact`
replacement-window support. Encrypted reasoning is not treated as a compaction
checkpoint. No native compaction request, history replacement, model change or
new provider credential path is enabled by this change. Native compaction needs
a separate transport capability, durable replay contract and interruption tests
before it can be accepted.

## Verification scope

Focused local tests cover profiles and missing usage, discovery, common provider
request fixtures, Code Mode confinement/deadline/cancellation, and a session
using a nested file read with final-output-only model input. Native screenshot
capture and image decoding were exercised on macOS. A compiled macOS ARM64 executable also completed a nested file read through Code
Mode against a local test provider. Interactive keyboard control, packaged worker
execution on other platforms, provider-native compaction and paid/live provider
behavior remain unverified. This is not a claim of complete computer
use or cache-performance equivalence.
4 changes: 2 additions & 2 deletions src/mendcode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.3",
"dompurify": "3.4.13",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"drizzle-kit": "1.0.0-beta.20",
"drizzle-orm": "1.0.0-beta.20",
"effect": "4.0.0-beta.59",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
Expand Down
3 changes: 2 additions & 1 deletion src/mendcode/packages/opencode/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,13 @@ for (const item of targets) {
: undefined,
},
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
entrypoints: ["./src/index.ts", parserWorker, workerPath, "./src/mend/codemode/worker.ts", ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
MENDCODE_CODE_WORKER_PATH: "./src/mend/codemode/worker.ts",
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
},
Expand Down
32 changes: 32 additions & 0 deletions src/mendcode/packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ const DEFAULT_VARIANT_VALUE = "default"

const log = Log.create({ service: "acp-agent" })

function smartReplyForPermission(permission: { metadata?: Record<string, unknown> }, grant?: "once" | "task") {
const facts = permission.metadata?.actionFacts
const authority = permission.metadata?.authorityContext
const actionFingerprint =
facts && typeof facts === "object" && typeof (facts as { fingerprint?: unknown }).fingerprint === "string"
? (facts as { fingerprint: string }).fingerprint
: typeof permission.metadata?.smartActionFingerprint === "string"
? permission.metadata.smartActionFingerprint
: undefined
const smartManaged = permission.metadata?.smartApproval === true
if (!actionFingerprint && !smartManaged) return undefined
const contextRevision =
authority &&
typeof authority === "object" &&
Number.isSafeInteger((authority as { contextRevision?: unknown }).contextRevision)
? (authority as { contextRevision: number }).contextRevision
: undefined
return {
...(actionFingerprint === undefined ? {} : { actionFingerprint }),
...(contextRevision === undefined ? {} : { contextRevision }),
...(grant ? { grant } : {}),
}
}

async function getContextLimit(
sdk: OpencodeClient,
providerID: ProviderID,
Expand Down Expand Up @@ -256,6 +280,14 @@ export class Agent implements ACPAgent {
await this.sdk.permission.reply({
requestID: permission.id,
reply: res.outcome.optionId as "once" | "always" | "reject",
smart: smartReplyForPermission(
permission,
res.outcome.optionId === "always"
? "task"
: res.outcome.optionId === "once" && permission.metadata?.smartApproval === true
? "once"
: undefined,
),
directory,
})
})
Expand Down
44 changes: 44 additions & 0 deletions src/mendcode/packages/opencode/src/cli/cmd/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { cmd } from "./cmd"
import { withSharedClient } from "../shared-client"
import { contextReport } from "@/session/context-profile"
import type { MessageV2 } from "@/session/message-v2"
import { SessionID } from "@/session/schema"

export const ContextCommand = cmd({
command: "context <sessionID>",
describe: "inspect recent context estimates, cache usage and request timing",
builder: (yargs) => yargs
.positional("sessionID", { type: "string", demandOption: true })
.option("limit", { type: "number", default: 20, describe: "recent messages to inspect (1–100)" })
.option("json", { type: "boolean", default: false, describe: "print the structured context report" }),
handler: async (args) => {
const id = SessionID.make(args.sessionID)
if (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > 100) throw new Error("limit must be an integer from 1 to 100")
const report = await withSharedClient(process.cwd(), async (connection) => {
const url = new URL(`/session/${encodeURIComponent(id)}/message`, connection.url)
url.searchParams.set("limit", String(args.limit))
url.searchParams.set("view", "tui")
url.searchParams.set("partsLimit", "100")
const response = await fetch(url, { headers: connection.headers, signal: AbortSignal.timeout(30_000) })
if (!response.ok) throw new Error(`Context report failed (HTTP ${response.status})`)
return contextReport(await response.json() as MessageV2.WithParts[])
})
if (args.json) {
console.log(JSON.stringify(report, null, 2))
return
}
console.log("Context and cache — recent messages")
if (!report.requests.length) {
console.log("No request profiles in this page. Profiles are recorded by the updated runtime on completed steps.")
return
}
for (const request of report.requests) {
const p = request.profile
const t = p.estimatedTokens
console.log(`\n${request.messageID} ${request.providerID}/${request.modelID}`)
console.log(`Estimated tokens: instructions ${t.instructions}, memory ${t.memory}, history ${t.history}, tool results ${t.toolResults}, schemas ${t.toolSchemas}, media ${t.media}; total ${t.total}`)
console.log(`Provider input: ${request.inputTokens ?? "unavailable"}; cache read: ${request.cachedTokens ?? "unavailable"}; cache write: ${request.cacheWriteTokens ?? "unavailable"}; hit rate: ${request.cacheHitRate === null ? "unavailable" : `${(request.cacheHitRate * 100).toFixed(1)}%`}`)
console.log(`First token: ${p.firstTokenMs == null ? "unavailable" : `${p.firstTokenMs} ms`}; request: ${p.durationMs ?? "unavailable"} ms; tools: ${p.toolCount}; images: ${p.imageCount}`)
}
},
})
Loading
Loading