You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix: plugin hook error handling (B58-B59) + BUGS.md cleanup
B58: pluginGuard() now catches Plugin.trigger() errors and returns
EditResult error instead of propagating uncaught exception.
B59: pluginNotify() now catches Plugin.trigger() errors and logs
warning instead of silently ignoring.
BUGS.md cleaned up:
- Removed duplicate "Open — Bugs (0)" section
- Added B58-B59 as fixed
- Added 10 new false positives from QA analysis (V1, R4, E1-fork,
PM1, PM2, etc.) — each verified with reasoning
- Updated Q3 reference from "this PR" to "PR #31"
- Total: 0 open bugs, 64 fixed, 15 false positives documented
* docs: QA round 3 — zero new bugs, 5 more false positives verified
Third round of deep analysis covering: session management, compaction,
prompt pipeline, skill/scripts, command templates, truncation.
All areas clean. All previous fixes (B48, B38, B57) verified holding.
New false positives (5):
- updatePart() orphaned parts → FK constraint prevents
- Script paths with spaces → array-based execution is safe
- Truncation boundary at maxBytes → comparison is correct
- Compaction during prompt → BusyError prevents
- filterEdited + sweep same part → orthogonal concerns
Total: 0 open bugs, 20 verified false positives.
* fix: B60 objective markdown injection + sweep error logging + integration proof tests
B60 (Med): Objective text injected directly into system prompt markdown.
Newlines and markdown chars (backticks, headers) could break formatting.
Fix: escape newlines and markdown special chars before injection.
Sweep error logging: Database.transaction() in sweep() now wrapped in
try-catch with log.error() instead of silent failure.
Integration proof tests (3 new):
- PROOF: hide() removes content from LLM context, CAS preserves original
- PROOF: unhide() restores original content from CAS
- PROOF: mark discardable + sweep removes content after N turns
Each test creates a real session with messages, performs the edit operation,
then verifies the content IS present before and IS NOT present after
(or vice versa for unhide). CAS storage verified independently.
Cross-module false positives documented (3 new):
- Protected message window timing → benign race
- Side thread system prompt staleness → fresh DB query per prompt
- Sweep transaction failure → now logged
tmux TUI tests: all 5 flows pass including LLM submit-message and cost-dialog.
* docs: add history editing prompts, slash commands, and verification guide
Added to docs/context-editing.md:
- Direct prompts to trigger context editing (hide, replace, externalize, mark, park)
- Complete slash command reference (/focus, /focus-rewrite-history, /btw, /reset-context, /classify, /threads, /history, /tree)
- How to enable focus agents in opencode.json config
- How history editing is verified (integration proof tests)
* fix: QA rounds 5-6 — B61-B64 fixes, 10 tmux flows, promptable agent switching
- B61: MCP add() inconsistent return type (Status vs Record) — all branches now return Record
- B62: text part timing start overwritten at stream end — preserve original start
- B63: unguarded JSON.parse on ripgrep output — flatMap with try-catch
- B64: untracked file line count off-by-one — trimEnd before split
- 5 new tmux test flows (slash-command, multi-agent-verify, slash-classify, slash-threads, slash-history) — 10 total
- Promptable agent mode switching: updated plan_enter/plan_exit tool descriptions for autonomous back-and-forth switching
- Documented mode switching flow in docs/agents.md
- 23 new false positives verified (49 total in BUGS.md)
| B53 |`CAS.deleteBySession()` race condition | High | Wrapped SELECT + DELETE in `Database.transaction()`|
31
-
| B54 |`CAS.deleteOrphans()` deletes shared CAS entries | High | Added EditGraphNode reference check before deleting |
32
-
| B55 |`EditGraph.checkout()` inconsistent on partial failure | High | Wrapped undo loop + head update in `Database.transaction()`|
33
-
| B56 |`EditGraph.deleteBySession()` not atomic | Med | Wrapped in `Database.transaction()`|
34
-
| B57 |`filterEdited()` synthetic placeholder reuses part ID | Med | Changed to `PartID.ascending()` for unique synthetic ID |
35
-
36
17
## Open — Edge Cases (1)
37
18
38
19
| # | Issue | Sev | Location | Notes |
39
20
| --- | ----- | --- | -------- | ----- |
40
-
| E1 |`sweep()` clock skew: `turnWhenSet > currentTurn`| Low |`context-edit/index.ts:622-625`| Negative elapsed → never sweeps. Only possible from a bug upstream — turn counter is monotonic. |
41
-
42
-
## False Positives — Edge Cases (5)
43
-
44
-
Investigated and determined to be correct behavior or non-issues.
21
+
| E1 |`sweep()` clock skew: `turnWhenSet > currentTurn`| Low |`context-edit/index.ts:622-625`| Negative elapsed → never sweeps. Turn counter is monotonic — only possible from upstream bug. |
45
22
46
-
| Issue | Verdict |
47
-
|-------|---------|
48
-
| E2: `EditGraph.getHead()` returns undefined vs null |**Correct** — `undefined` is standard TS for "not present"; all callers use `!head` which handles both |
49
-
| E3: First commit creates self-referential branch |**Intentional** — `branches: { main: nodeID }` is standard DAG initialization; "main" → first node is correct |
| E5: `SideThread.create()` duplicate ID not caught |**Correct** — `Identifier.ascending()` is unique (timestamp+counter+random); DB error on collision is the right behavior (fail loudly) |
52
-
| E6: SHA-256 collision in CAS not detected |**Intentional** — SHA-256 has no known collisions; `onConflictDoNothing()` was explicitly chosen (B43 fix) |
53
-
54
-
## Open — Code Quality (5)
55
-
56
-
Found during QA bug hunt (static analysis). Not crashes, but code quality issues.
23
+
## Open — Code Quality (4)
57
24
58
25
| # | Issue | Sev | Location | Notes |
59
26
| --- | ----- | --- | -------- | ----- |
60
-
| Q1 | 95 empty `.catch(() => {})` blocks across 29 files | Low | Various | Most intentional (file ops), ~10 mask real errors in `config.ts`, `lsp/client.ts`, `sdk.tsx`|
61
-
| Q2 | 17 TODO/FIXME/HACK comments | Low | 13 files | Track as tech debt; key ones: copilot lost type safety (#374), process.env vs Env.set (#300, #524) |
62
-
| Q3 |`console.log` in TUI production code | Low |`cli/cmd/tui/`|**FIXED** in this PR — replaced 18 calls with `Log.create()`|
63
-
| Q4 | Copilot SDK lost chunk type safety | Med |`provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts:374`| TODO says "MUST FIX" — type safety lost on Chunk due to error schema |
64
-
| Q5 |`process.env` used directly instead of `Env.set`| Low |`provider/provider.ts:300,524`| Env.set only updates shallow copy, not process.env — architectural issue |
65
-
66
-
## Open — Bugs (0)
67
-
68
-
_No open bugs._
27
+
| Q1 | 95 empty `.catch(() => {})` blocks across 29 files | Low | Various | Most intentional (file ops), ~10 mask real errors |
|`updatePart()` creates orphaned parts if message deleted | False positive — FK constraint `message_id → MessageTable.id` prevents orphaned inserts |
98
+
| Script paths with spaces in skill/scripts.ts | False positive — array-based `Process.text()` doesn't split on spaces |
99
+
| Truncation boundary at exact maxBytes | False positive — `>` comparison is correct (include at limit, truncate above) |
100
+
| Compaction during active prompt | False positive — `BusyError` prevents concurrent runs |
101
+
| filterEdited + sweep modify same part | False positive — orthogonal concerns (edit vs lifecycle), no conflict |
102
+
| Nested `Database.use()` in `checkout()` transaction | False positive — `Database.use()` reuses transaction context via ALS `ctx.use()`|
103
+
| Uninitialized `casHash` in hide/replace/externalize | False positive — `Database.transaction()` callback is synchronous, always assigns before outer scope |
104
+
|`CAS.store` race in `externalize()`| False positive — both store and get run synchronously within same transaction |
105
+
|`filterEdited` synthetic part losing agent metadata | False positive — message spread `...msg` preserves role/agent; part is just text placeholder |
106
+
|`annotate()` losing `casHash` from previous edit | False positive — spread `...part.edit` preserves all existing fields including casHash |
107
+
| Side-thread `update()` read-after-write staleness | False positive — SQLite ops are synchronous; `get()` sees committed data |
108
+
| Provider `find("create")!` non-null assertion | Inside try-catch; degrades to `InitError` with cause — confusing but not a crash |
109
+
| Permission `Map.delete` during iteration | False positive — safe per JS Map spec; deleted entries not revisited |
110
+
| Permission data `null ?? []` fallback | False positive — `??` correctly handles both `null` and `undefined`|
111
+
| Retry `JSON.parse` without string check | False positive — entire block wrapped in try-catch returning undefined |
112
+
| Instruction state Map unbounded growth | False positive — `clear()` called per message; states keyed by directory (few entries) |
113
+
| Provider sort `findIndex` returning -1 | False positive — desc sort puts -1 last, non-matching models sort after all priority models |
114
+
| Bash tool double-kill on timeout+abort | False positive — timeout `.catch(() => {})` is intentional; double-kill is idempotent |
115
+
| Edit tool sync stat then async read TOCTOU | False positive — `Filesystem.stat()` is synchronous; TOCTOU benign (caught by readText) |
116
+
| Share sync queue data loss on rapid calls | False positive — Map mutation visible to timeout closure; all merged data sent |
117
+
|`side-thread` hint ignored by sweeper | By design — side-thread is a classification hint for `/focus`, not auto-cleanup |
118
+
| Compaction loses edit metadata on replay | False positive — replay only replays user messages; user messages can't be edited (ownership check) |
119
+
|`Database.effect()` async fire-and-forget | Intentional — effects fire after DB commit; `Bus.publish` async rejection is benign since DB state is already correct |
120
+
| Share sync timeout accumulation | False positive — exactly 1 timeout per sessionID; existing entry merges data, no new timer created |
**TUI Testing:** Use `testRender()` from `@opentui/solid` for unit tests. tmux-based integration harness at `test/cli/tui/tmux-tui-test.ts` for E2E flows.
131
+
**TUI Testing:**`testRender()` for components, tmux harness at `test/cli/tui/tmux-tui-test.ts` for E2E. 10 flows pass (home, command-palette, agent-cycle, submit-message, cost-dialog, slash-command, multi-agent-verify, slash-classify, slash-threads, slash-history).
132
+
133
+
**SAST:** Pre-commit + CI run `scripts/sast-check.sh` (no eval, no Function, no secrets, no console.log).
Copy file name to clipboardExpand all lines: docs/agents.md
+51Lines changed: 51 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,6 +42,57 @@ Complete conversation rewrite agent. Asks user to confirm objective before proce
42
42
43
43
Invoked via `/focus-rewrite-history` command. Always asks for confirmation before rewriting user messages.
44
44
45
+
## Promptable Mode Switching
46
+
47
+
Build and Plan agents can be switched via natural language prompts — the same way Claude Code supports "enter plan mode". The LLM calls the appropriate tool, the user confirms, and the TUI updates automatically.
48
+
49
+
### How it works
50
+
51
+
| Direction | Tool | Permission | TUI Update |
52
+
|-----------|------|------------|------------|
53
+
| Build → Plan |`plan_enter`| Build agent has `plan_enter: "allow"`|`local.agent.set("plan")`|
54
+
| Plan → Build |`plan_exit`| Plan agent has `plan_exit: "allow"`|`local.agent.set("build")`|
55
+
56
+
**Flow:**
57
+
1. User types a natural language prompt, OR the agent decides autonomously that switching would be beneficial
58
+
2. The current agent calls `plan_enter` (Build → Plan) or `plan_exit` (Plan → Build)
59
+
3. A confirmation dialog appears asking the user to approve the switch
60
+
4. On approval, a synthetic user message is created with `agent: "plan"` or `agent: "build"`, switching the active agent
61
+
5. The TUI watcher in `session/index.tsx` detects the completed tool call and updates the agent display
62
+
6. Subsequent messages use the new agent's system prompt and permissions
63
+
64
+
### Autonomous switching
65
+
66
+
Agents can decide to switch modes based on their own reasoning — they do not need the user to explicitly ask. The Build agent will proactively switch to Plan when it determines a task is complex enough to benefit from planning. The Plan agent will switch to Build when planning is complete and implementation should begin. Agents can switch back and forth as many times as needed during a session.
67
+
68
+
**Build → Plan (autonomous):** The Build agent realizes mid-implementation that the task is more complex than expected, involves multiple files, or requires architectural decisions. It calls `plan_enter` to step back and plan first.
69
+
70
+
**Plan → Build (autonomous):** The Plan agent completes the plan file, has no remaining questions, and determines the plan is ready. It calls `plan_exit` to begin implementation.
71
+
72
+
### Example prompts (user-triggered)
73
+
74
+
**Switch to Plan mode:**
75
+
```
76
+
Let's plan this before implementing.
77
+
Enter plan mode.
78
+
I need to think through the architecture first.
79
+
```
80
+
81
+
**Switch back to Build mode:**
82
+
```
83
+
The plan looks good, let's implement it.
84
+
Start building.
85
+
Exit plan mode and execute the plan.
86
+
```
87
+
88
+
### Implementation details
89
+
90
+
-**Tools:**`plan_enter` and `plan_exit` defined in `src/tool/plan.ts`
91
+
-**TUI watcher:**`src/cli/cmd/tui/routes/session/index.tsx:221-236` listens for tool completions
92
+
-**Permissions:** Build agent allows `plan_enter`; Plan agent allows `plan_exit` (cross-permissions)
93
+
-**Confirmation:** Both tools use `Question.ask()` to get user consent before switching
94
+
-**Plan file:** Stored at `$XDG_DATA_HOME/opencode/plans/<session-slug>.md`
Copy file name to clipboardExpand all lines: docs/context-editing.md
+68Lines changed: 68 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,6 +90,74 @@ Park and list project-level side threads. Threads survive across sessions.
90
90
91
91
---
92
92
93
+
## How to Elicit History Editing
94
+
95
+
The context editing system is available to the agent when `context_edit` is in the tool set. The agent can use it autonomously, or you can prompt it directly.
96
+
97
+
### Direct prompts to trigger editing
98
+
99
+
**Hide stale content:**
100
+
```
101
+
Hide the file read output from 3 turns ago — it's outdated since we edited the file.
102
+
```
103
+
104
+
**Replace incorrect information:**
105
+
```
106
+
The grep result from earlier is wrong — replace it with a note saying "file was restructured".
107
+
```
108
+
109
+
**Externalize verbose output:**
110
+
```
111
+
Externalize that long test output — just keep a summary of what passed and failed.
112
+
```
113
+
114
+
**Mark for automatic cleanup:**
115
+
```
116
+
Mark that debug logging as discardable — it's only useful for the next 2 turns.
117
+
```
118
+
119
+
**Park a side thread:**
120
+
```
121
+
Park that security issue we noticed — it's not related to our current task.
122
+
```
123
+
124
+
### Slash commands
125
+
126
+
| Command | What it does |
127
+
|---------|-------------|
128
+
|`/focus`| Runs the classifier agent to label messages by topic, then externalizes stale output and parks off-topic threads. Requires the focus agent to be enabled in config. |
129
+
|`/focus-rewrite-history`| Full conversation rewrite with user confirmation. The agent reviews all messages, classifies them, and rewrites the history to focus on the current objective. Disabled by default — enable in agent config. |
130
+
|`/btw <question>`| Ask a side question without polluting the main conversation. Runs in a forked ephemeral session. |
131
+
|`/reset-context`| Restore all edited parts to their originals from CAS. Undo all context edits. |
132
+
|`/classify`| Run the classifier agent to see how messages are labeled (main/side/mixed). Read-only, no side effects. |
133
+
|`/threads`| List all parked side threads for this project. |
134
+
|`/history`| Show the edit history (linear log from HEAD). |
135
+
|`/tree`| Show the full edit DAG with branches. |
136
+
137
+
### Enabling focus agents
138
+
139
+
By default, the focus and focus-rewrite-history agents are disabled. Enable them in your `opencode.json`:
0 commit comments