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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,35 @@ The [20 sample goals](examples/) include 12 runnable coding exercises and eight
`src/contract.ts` defines parsing and locking; `checks.ts` runs verifiers; `engine.ts` owns state transitions; `coordinator.ts` owns continuation admission. `server.ts` integrates the OpenCode 2 API. `tui.ts` renders read-only status. `rpc.ts` is the native sidebar's portable status contract.

See [research](docs/research.md), [architecture](docs/architecture.md), [contract design](docs/contracts.md), and [contributing](CONTRIBUTING.md). MIT licensed.

## Rolling image context window

Master Goal limits raw images sent with each model request. The default is **1**, so screenshot-heavy sessions do not accumulate every previous screenshot in the provider request. The newest images across the whole outgoing conversation survive, including screenshots returned by tools. Text, code, reasoning, other files and tool-call/result pairs remain.

Use OpenCode 2.0.22's native plugin options in `opencode.json` or `opencode.jsonc`. Replace the existing Master Goal entry; do not add a second registration. Keep its existing `package` path (the installer normally uses a `file:///.../.mastergoal-runtime/node_modules/@darkmatter2222/opencode-mastergoal` directory URL):

```json
{
"plugins": [
{
"package": "@darkmatter2222/opencode-mastergoal",
"options": { "imageWindow": 1 }
}
]
}
```

| Value | Behavior |
| ------------- | ---------------------------------------------- |
| `1` (default) | Latest image only |
| `2` | Latest two images |
| `4` | Latest four images |
| `0` | No raw images, including newly attached images |

Explicit `options.imageWindow` takes precedence over `MASTERGOAL_IMAGE_WINDOW`, then the default `1`. Options require a non-negative safe integer JSON number; the environment variable requires decimal digits. Invalid selected configuration rejects plugin startup with a clear error, never disables the ceiling. Restart OpenCode after changing settings. Installer upgrades preserve plugin options.

Protection applies whenever the plugin is loaded for a location, including sessions without a goal, finite and forever goals, autonomous continuation, compaction, title and session-generation requests. It filters the canonical request before provider serialization and does not delete persisted screenshots. It does not change the model architecture or vLLM's own multimodal limit. Set the window at or below your server's image limit.

The sidebar and `/goal status` show `Image window: N`. When pruning occurs, debug output contains counts only: `[mastergoal] image window: found=100 retained=1 removed=99 limit=1`. No image payloads or URLs are logged. This is runtime configuration, separate from `goal.md` and its contract hash.

The guarantee covers canonical image parts in these OpenCode session request hooks. Another plugin that adds images **after** Master Goal's hook can invalidate it; avoid such conflicting transforms. Opaque provider-managed history, images embedded inside PDFs, and model calls made outside OpenCode's session hooks are not inspected or decoded.
14 changes: 14 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,17 @@ The sidebar is read-only. It never alters host todos to create a cosmetic succes
`/goal construct` is an explicitly requested preparation turn using the host's current model and tools. It accepts ordinary Markdown without a pre-existing contract. The original document and construction instructions are saved outside the project in session state and reinjected during context creation and compaction. No active, paused, or blocked run may enter construction; stop that run first. Successful start clears the construction context and pins the generated contract through the normal engine. Construction itself cannot mark a goal complete.

The model edits draft files through OpenCode's ordinary tools and permission system. This phase is intentionally distinct from the locked verification loop: it may ask questions or need another turn. Generated acceptance criteria require inspection; neither the parser nor a model can establish semantic equivalence between arbitrary prose and executable checks. Optional reviewer scripts still require their own configured service and may only veto deterministic success.

## Rolling image window (OpenCode 2.0.22)

Persistent history is translated by `SessionModelRequest.baseTranscript` / `toLLMMessages` into canonical `@opencode/ai` messages. The existing `context` and `compaction` callbacks now apply `applyImageWindow` and then inject goal context. Separate `generate` and `title` hooks filter auxiliary session requests because those flows do not trigger `context`. The host builds its `LLMRequest` from the modified messages before protocol serialization; its later unsupported-media and byte-budget passes cannot add images.

`src/image-window.ts` walks messages and content backwards, retaining the newest N images globally, then reconstructs changed containers in normal order. Complexity is O(messages + parts), including tool-result content. There is no image decoding or network access. Retained `Media.Asset` instances and large payloads are shared; source arrays/messages/results are never mutated. Image-only messages emptied by filtering are removed. An image-only tool result becomes a text-only removal notice so its matching tool call remains valid.

The exact image representations are `media` parts whose `Media.Asset.kind` is `image` or `mediaType` starts with `image/`, and `tool-result.result` with `type: content` containing `file` items with an `image/` MIME type. Assets support bytes, base64, URLs and provider references. The host converts user attachments to media assets and tool/MCP screenshot outputs to file content before these hooks. JSON/text that merely mentions an image is not media. PDFs/audio/source files remain untouched.

Native `plugins: [{ package, options: { imageWindow: 1 } }]` supplies `ctx.options`. The selected value is validated at startup, with explicit options over environment over default. It is not goal state and does not affect locking or lifecycle semantics. RPC appends the effective value for the sidebar. Installer replacement preserves options.

Source inspection: upstream v2 commit `f74512bfe0ad5264acd58ab6d7548eae427f75f5` (package version 2.0.22), `packages/core/src/session/model-request.ts`, `runner/to-llm-message.ts`, `packages/plugin/src/promise/session.ts`, `packages/schema/src/config/plugin.ts`, and the installed 2.0.22 AI media/tool schemas and protocol lowering. No dependency upgrade was required.

This is request-context filtering, not persistent-history cleanup. No universal final-hook priority exists in this API: later third-party hooks must not reinsert media. Opaque provider-side history and external model calls lie outside this canonical raw-image boundary.
23 changes: 23 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,26 @@ The host canary starts an isolated local fake provider and temporary OpenCode st
## Limits of this evidence

The six-platform/node CI matrix and real-host job passed on GitHub for the Windows process-cleanup fix (run 37157718683). The current workflow additionally tests packaged installation on Windows with Node 22.18.0 and npm 10.9.3. Windows process-tree cancellation is limited to the direct child. Live cloud-provider reliability, weeks-long runs, genuine coding-task success rate, model-specific compaction quality, malicious same-user shell attacks, and active-active server operation are not certified by these tests. This release is version 0.3.0; report reproducible host/runtime issues with the locked contract and redacted state.

## Rolling image window — 2026-10-04 UTC

Pinned `@opencode/plugin@2.0.22` remains unchanged. Validation ran on Node 24.19.0 with the actual OpenCode 2.0.22 Linux x64 binary.

- `npm run verify`: 117 tests passed; 20/20 deterministic benchmark scenarios passed.
- `npm run test:install`: packaged local/global install, durable runtime and uninstall passed.
- `scripts/host-canary-v2.mjs`: admitted 100 distinct image-bearing user prompts with execution initially suspended, then started a finite goal. Three provider HTTP requests each contained exactly one image with no configuration supplied. All 100 screenshot filenames survived session export; construction, continuation, usage and sidebar RPC also passed.
- The same host canary with `CANARY_IMAGE_WINDOW=4` captured `[4, 4, 4]`; with `CANARY_IMAGE_WINDOW=0`, `[0, 0, 0]`. Both preserved all 100 persisted attachments.
- The canary additionally compares the final retained image URL against a distinct newest PNG. Automatic compaction is disabled in this fixture to prove pruning happens independently of compaction. The fake model does not implement the host's summary template. Compaction/title/generate filtering and existing goal injection are exercised at the official hook boundary by adapter tests.
- Unit tests cover 1,000 growing image-bearing turns, limits 0/1/2/4, ordering within/across messages, native `Media.Asset` sources, tool-result screenshot files, empty-image results, unusual non-image content, frozen source history, strict configuration and upgrade preservation.

Reproduce after `npm ci`:

```sh
npm run verify
npm run test:install
node scripts/host-canary-v2.mjs /absolute/path/to/opencode
CANARY_IMAGE_WINDOW=4 node scripts/host-canary-v2.mjs /absolute/path/to/opencode
CANARY_IMAGE_WINDOW=0 node scripts/host-canary-v2.mjs /absolute/path/to/opencode
```

The provider capture uses OpenAI-compatible HTTP, not a live vLLM model. Other provider formats are covered by the shared canonical API implementation, not by independent live-provider tests. Later third-party hook rewrites and opaque provider-managed image history are outside this plugin's enforceable boundary; see architecture notes.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darkmatter2222/opencode-mastergoal",
"version": "0.3.0",
"version": "0.4.0",
"description": "OpenCode 2.x goal orchestration with locked deterministic completion contracts",
"type": "module",
"license": "MIT",
Expand Down
41 changes: 41 additions & 0 deletions releases/v0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Rolling image context window

Master Goal now defaults to sending only the newest raw image with each model request. Screenshot-heavy sessions can retain their history without sending every previous screenshot back to the model.

- Set native plugin `options.imageWindow` to 1 (default), 2, 4, or any non-negative safe integer. Zero excludes all raw images.
- Enforced before provider serialization for normal, autonomous, compaction, title and session-generation requests, including sessions without an active goal.
- Handles user attachments and tool/MCP screenshot files. Preserves text, non-image files, tool-call/result pairs and persistent screenshots.
- Native plugin options override `MASTERGOAL_IMAGE_WINDOW`; invalid selected settings reject startup. Installer upgrades preserve options.
- Sidebar/status displays the limit; pruning diagnostics contain counts only.
- OpenCode plugin dependency remains pinned to 2.0.22. Goal completion, forever mode and failure handling are unchanged.

## Update

Close OpenCode, then run:

```powershell
opencode service stop
npx --yes --package=github:darkmatter2222/opencode_mastergoal#v0.4.0 mastergoal install --global
opencode
```

Use `--local` instead of `--global` for a project installation. Existing installations need no new setting to get the default limit of one.

To configure the window, change your existing Master Goal registration in `opencode.json[c]` from its package string to an object, retaining that exact package path:

```json
{
"package": "file:///YOUR-EXISTING-PATH/.mastergoal-runtime/node_modules/@darkmatter2222/opencode-mastergoal",
"options": { "imageWindow": 2 }
}
```

Restart OpenCode after changing the setting. This does not change your inference server's own image limit.

## Validation and scope

Local verification passed 117 tests and 20 deterministic scenarios; packaged local/global install and uninstall passed. Actual OpenCode 2.0.22 captured three provider requests with image counts `[1,1,1]` from 100 stored image-bearing prompts. Configured limits 4 and 0 produced `[4,4,4]` and `[0,0,0]`. All 100 attachments remained in session export, and the default retained PNG was verified as the newest. Unit endurance coverage includes 1,000 growing turns.

Release publication requires successful cross-platform CI on the exact release commit. The provider canary uses a local OpenAI-compatible fake, not a live vLLM model. Other plugins must not insert images after this filter. Opaque provider-side history and model calls outside OpenCode session hooks are outside its scope.

The attached tarball contains the compiled runtime, with SHA256SUMS. This is a GitHub release; the package is not published to the npm registry.
72 changes: 65 additions & 7 deletions scripts/host-canary-v2.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,25 @@ const env = {
const exec = promisify(execFile);
let constructionCalls = 0;
let calls = 0;
const imageRequests = [];
const imageLimit = Number(process.env.CANARY_IMAGE_WINDOW ?? 1);
const png =
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAFElEQVR4nGPUCKhgwAaYsIoOWgkAwFgBALQ78QgAAAAASUVORK5CYII=";
const imageURI = `data:image/png;base64,${png}`;
const newestURI =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAFElEQVR4nGOcJinHgA0wYRUdtBIApg4A3ZmXOwIAAAAASUVORK5CYII=";
const newestImages = [];
const server = createServer(async (req, res) => {
let body = "";
for await (const chunk of req) body += chunk;
const data = JSON.parse(body);
if (body.includes("image-history-canary")) {
const images = data.messages
.flatMap((m) => (Array.isArray(m.content) ? m.content : []))
.filter((p) => p.type === "image_url");
imageRequests.push(images.length);
newestImages.push(images.at(-1)?.image_url?.url);
}
if (body.includes("MASTER GOAL CONSTRUCTION")) constructionCalls++;
else calls++;
if (calls >= 3) await writeFile(root + "/accepted.txt", "verified");
Expand Down Expand Up @@ -104,13 +119,19 @@ const api = async (method, p, data) => {
try {
const config = {
plugins: [
pathToFileURL(
process.env.MASTERGOAL_PACKAGE_ROOT
? path.resolve(process.env.MASTERGOAL_PACKAGE_ROOT)
: fileURLToPath(new URL("../", import.meta.url)),
).href,
{
package: pathToFileURL(
process.env.MASTERGOAL_PACKAGE_ROOT
? path.resolve(process.env.MASTERGOAL_PACKAGE_ROOT)
: fileURLToPath(new URL("../", import.meta.url)),
).href,
...(process.env.CANARY_IMAGE_WINDOW === undefined
? {}
: { options: { imageWindow: imageLimit } }),
},
],
model: "canary/fake",
compaction: { auto: false },
providers: {
canary: {
package: "@ai-sdk/openai-compatible",
Expand All @@ -120,7 +141,11 @@ try {
apiKey: "fake",
},
models: {
fake: { name: "Fake", limit: { context: 100000, output: 1000 } },
fake: {
name: "Fake",
capabilities: { input: ["text", "image"] },
limit: { context: 100000, output: 1000 },
},
},
},
},
Expand Down Expand Up @@ -191,6 +216,15 @@ try {
model: { providerID: "canary", id: "fake" },
});
const session = created.data ?? created;
// Admit 100 separate image-bearing prompts without waking the model.
for (let i = 0; i < 100; i++)
await api("POST", `/api/session/${session.id}/prompt`, {
text: `image-history-canary ${i}`,
files: [
{ uri: i === 99 ? newestURI : imageURI, name: `screen-${i}.png` },
],
resume: false,
});
await api("POST", `/api/session/${session.id}/command`, {
name: "goal",
text: "start",
Expand All @@ -201,12 +235,32 @@ try {
while (Date.now() < deadline) {
result = await engine.store(session.id).read();
if (result?.status === "completed") break;
if (result?.status === "blocked") throw Error(JSON.stringify(result));
if (result?.status === "blocked")
throw Error("Host canary goal blocked; inspect isolated host logs");
await new Promise((r) => setTimeout(r, 100));
}
assert.equal(result?.status, "completed", JSON.stringify({ result, calls }));
assert(calls >= 3);
assert(result.usage.output > 0);
assert(
imageRequests.length > 0,
"Image-bearing history reached the provider",
);
assert(
imageRequests.every((n) => n === imageLimit),
JSON.stringify({ imageRequests, imageLimit }),
);
assert(
newestImages.every((uri) =>
imageLimit === 0 ? uri === undefined : uri === newestURI,
),
"Newest screenshot survives provider serialization",
);
const persisted = JSON.stringify(
await api("GET", `/api/experimental/session/${session.id}/export`),
);
for (let i = 0; i < 100; i++)
assert(persisted.includes(`screen-${i}.png`), `Persisted image ${i}`);
const rpc = await api(
"POST",
"/api/rpc/mastergoal.status/read?location[directory]=" +
Expand All @@ -225,6 +279,10 @@ try {
iterations: result.iterations,
outputTokens: result.usage.output,
rpc: true,
historicalImages: 100,
providerImageCounts: imageRequests,
persistedImagesPreserved: true,
newestImageVerified: true,
},
null,
2,
Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function initGoal(root: string, file = "goal.md") {
return `Created ${file}. Write your objective in ${file}, then /goal construct ${file}, then /goal start ${file}.`;
}
export const help =
"/goal init [path] | construct [path] | start [path] | forever [path] | status | inspect | check | pause | resume | stop | end";
"/goal init [path] | construct [path] | start [path] | forever [path] | status | inspect | check | pause | resume | stop | end\nRaw images per model request: imageWindow defaults to 1 (2 = latest two, 0 = no raw images). Configure plugins[].options.imageWindow in opencode.json[c], or MASTERGOAL_IMAGE_WINDOW; restart OpenCode. Session history is preserved.";
export async function command(
engine: Engine,
session: string,
Expand Down
Loading
Loading