Skip to content
Open
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
20 changes: 20 additions & 0 deletions devlog/2026-08-05_debug-nudge-phantom-loop/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# REQ — Debug Nudge Phantom Turn Loop

## Problem

When `config.debug: true`, ACP's debug notifications used `sendIgnoredMessage()` to persist nudge text and compress notifications to the conversation DB as `ignored: true` user messages. opencode's runtime loop detects the last user message by `role == "user"` WITHOUT checking the `ignored` flag, so the phantom user message triggers a new turn → model keeps working → calls compress → another notification → infinite loop.

## Evidence

From issue #20 floor 1734 (awork investigation):
- 867 messages in session, 10 compress calls, 10 `ignored: true` user messages
- Each phantom user message made the runtime loop continue
- Session eventually stopped but was stuck in a loop for extended period

## Fix

Remove ALL `sendIgnoredMessage` calls from debug-mode notification paths:
1. `hooks.ts:205-232` — Debug nudge callback: removed `sendIgnoredMessage`, kept `logger.debug` + `showToast`
2. `notification.ts:281-285` — Debug compress notification: removed `sendIgnoredMessage`, kept `showToast` + added `logger.debug`

Both sites now use `logger.debug` (file log at `~/.config/opencode/logs/acp/`) + `showToast` (5-second popup) for debug visibility, without writing to the conversation DB.
13 changes: 13 additions & 0 deletions devlog/2026-08-05_debug-nudge-phantom-loop/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# WORKLOG — Debug Nudge Phantom Turn Loop

## Changes

1. `lib/hooks.ts`: Removed `sendIgnoredMessage` from debug nudge callback (lines 205-220). Removed unused import (line 40). Kept `logger.debug` + `showToast`.
2. `lib/ui/notification.ts`: Replaced `sendIgnoredMessage` call with `logger.debug` in debug compress notification block (lines 281-285).

## Verification

- TypeScript: 0 errors
- Tests: 954 pass, 0 fail
- Build: 386.21 KB
- Deployed to `~/.cache/opencode/packages/opencode-acp@latest/`
20 changes: 4 additions & 16 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { applyMessageFilters } from "./messages/filter/apply"
import { ensureBuiltinFiltersRegistered } from "./messages/filter/builtin"
import { createSessionState, saveSessionState, syncToolCache, updatePerTurnState, type SessionStateRegistry } from "./state"
import { cacheSystemPromptTokens } from "./ui/utils"
import { sendIgnoredMessage } from "./ui/notification"
import { runBatchCleanup } from "./gc/merge"
import { getCurrentTokenUsage } from "./token-utils"

Expand Down Expand Up @@ -203,22 +202,11 @@ export function createChatMessageTransformHandler(
compressionPriorities,
config.debug
? (text: string) => {
// sendIgnoredMessage writes an ignored:true user msg to DB.
// opencode's runtime loop detects it as "last user" (role-only,
// ignores the flag) → phantom turn → compress → notification →
// infinite loop. Use logger.debug + toast instead.
logger.debug(`[ACP Debug] Nudge injected:\n${text}`)
if (state.sessionId && lastUserMessage) {
const userInfo = lastUserMessage.info as any
sendIgnoredMessage(
client,
state.sessionId,
`[ACP Debug Nudge]\n${text}`,
{
providerId: userInfo.model?.providerID,
modelId: userInfo.model?.modelID,
agent: userInfo.agent,
variant: userInfo.variant,
},
logger,
).catch(() => {})
}
client.tui
.showToast({
body: {
Expand Down
7 changes: 4 additions & 3 deletions lib/ui/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,11 @@ export async function sendCompressNotification(
toastMessage =
config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage)

// [DEBUG] Use toast + logger only. Do NOT use sendIgnoredMessage — it writes
// an ignored:true user msg to DB that opencode's runtime loop detects as
// "last user" (role-only) → phantom turn → infinite loop (issue #20 floor 1734).
if (config.debug) {
const chatMessage =
config.pruneNotification === "minimal" ? message : truncateToastBody(message)
await sendIgnoredMessage(client, sessionId, chatMessage, params, logger)
logger.debug(`[ACP Debug] Compress notification:\n${message}`)
}

await client.tui.showToast({
Expand Down
Loading