Merge pull request #4096 from code-yeongyu/kimi-k2.6
fix: add merge-conflict guard test to prevent source file corruption
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
# Debugging Journal — Race Condition Hang Between opencode and omo
|
||||
|
||||
**Date:** 2026-05-16
|
||||
**Goal:** Investigate and fix a race condition / infinite hang bug between opencode (../opencode) and omo that causes prompting to hang indefinitely.
|
||||
|
||||
## Phase 0 — Environment Assessment
|
||||
|
||||
- **OMO repo:** `/Users/yeongyu/local-workspaces/omo` (plugin for OpenCode)
|
||||
- **Opencode repo:** `/Users/yeongyu/local-workspaces/opencode` (OpenCode server/SDK)
|
||||
- **Worktree:** `/Users/yeongyu/local-workspaces/omo-kimi-k2.6`
|
||||
- **Runtime:** Bun (omo), Node/Bun (opencode with Effect 4.0.0-beta.65)
|
||||
|
||||
## Phase 1 — Hypothesis Formation
|
||||
|
||||
### Hypothesis 1: promptAsync dispatch timeout not covering hanging fetch
|
||||
- `promptAsyncAfterSessionIdle` wraps `session.promptAsync()` with `withDispatchTimeout` (default 30s)
|
||||
- But `Promise.race` doesn't cancel the underlying fetch — it just returns after timeout
|
||||
- The reservation is then held for `postDispatchHoldMs` (250ms) before expiring
|
||||
- **BUT:** If the event loop is blocked, `setTimeout` won't fire, so both promises hang
|
||||
|
||||
### Hypothesis 2: Effect-native event system in opencode has race condition
|
||||
- Opencode commit `e11e089e4` (May 14) added Effect-native core event system
|
||||
- OMO commit `b333a5280` (May 16) added dispatch timeout to prompt-async-gate
|
||||
- The hang persists after both fixes
|
||||
- The `promptAsync` handler in opencode uses `Effect.forkIn(scope, { startImmediately: true })`
|
||||
- If `forkIn` has a bug in Effect 4.0.0-beta.65, the HTTP response might not return
|
||||
|
||||
### Hypothesis 3: Reservation leak in prompt-async-gate
|
||||
- If `dispatchAfterSessionIdle` throws before `dispatchAttempted = true`, the finally block deletes the reservation
|
||||
- If `dispatchAttempted = true` but `postDispatchHoldMs` is very large, reservation stays until `pruneExpiredReservations` runs
|
||||
- But default is 250ms, so this should not cause "forever" hang
|
||||
|
||||
## Phase 2 — Parallel Investigation
|
||||
|
||||
### Key Files Read
|
||||
- `omo/src/shared/prompt-async-gate.ts` — The gate logic with timeout
|
||||
- `omo/src/shared/session-idle-settle.ts` — Simple settle logic
|
||||
- `omo/src/plugin/event.ts` — Event handler that calls `autoContinueAfterFallback`
|
||||
- `opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts` — Server-side `promptAsync` handler
|
||||
- `opencode/packages/opencode/src/session/prompt.ts` — SessionPrompt service with `loop()`
|
||||
- `opencode/packages/core/src/event.ts` — Effect-native event system
|
||||
|
||||
### Key Findings
|
||||
1. OMO `promptAsyncAfterSessionIdle` has 30s dispatch timeout (added May 16)
|
||||
2. Opencode server `promptAsync` handler forks prompt processing into a scope
|
||||
3. Opencode uses Effect 4.0.0-beta.65 — a beta version
|
||||
4. The `promptSvc.prompt()` calls `loop()` which has `while (true)`
|
||||
5. The SDK `createOpencodeClient` sets `req.timeout = false` on fetch
|
||||
|
||||
## Next Steps
|
||||
1. Check for any OMO callers that bypass the gate (raw `session.promptAsync` calls)
|
||||
2. Check opencode logs for hanging requests
|
||||
3. Create a reproduction test
|
||||
4. Fix the root cause
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { readdirSync, readFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
|
||||
function* walk(dir: string): Generator<string> {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
||||
continue
|
||||
}
|
||||
yield* walk(path)
|
||||
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".json"))) {
|
||||
yield path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasConflictMarkers(content: string): boolean {
|
||||
const lines = content.split("\n")
|
||||
return lines.some((line) =>
|
||||
line.startsWith("<<<<<<< ") ||
|
||||
line === "=======" ||
|
||||
line.startsWith(">>>>>>> ")
|
||||
)
|
||||
}
|
||||
|
||||
describe("#given source files in src/", () => {
|
||||
test("#then no file contains unresolved git merge conflict markers", () => {
|
||||
const conflicts: string[] = []
|
||||
for (const path of walk(join(import.meta.dir, "../../../src"))) {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
if (hasConflictMarkers(content)) {
|
||||
conflicts.push(path)
|
||||
}
|
||||
}
|
||||
expect(conflicts).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user