fix(codeagent-wrapper): add timeout for Windows process termination

- Add forceKillWaitTimeout (5s) to prevent cmd.Wait() blocking forever
- Enhance sendTermSignal with killProcessTree fallback using wmic
- Update omo README: remove sisyphus, fix model names, update config

Fixes #115

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
cexll
2026-01-14 10:43:25 +08:00
parent 4395c5785d
commit 8f05626075
3 changed files with 129 additions and 22 deletions

View File

@@ -17,6 +17,7 @@ import (
) )
const postMessageTerminateDelay = 1 * time.Second const postMessageTerminateDelay = 1 * time.Second
const forceKillWaitTimeout = 5 * time.Second
// commandRunner abstracts exec.Cmd for testability // commandRunner abstracts exec.Cmd for testability
type commandRunner interface { type commandRunner interface {
@@ -1109,7 +1110,8 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
waitLoop: waitLoop:
for { for {
select { select {
case waitErr = <-waitCh: case err := <-waitCh:
waitErr = err
break waitLoop break waitLoop
case <-ctx.Done(): case <-ctx.Done():
ctxCancelled = true ctxCancelled = true
@@ -1120,8 +1122,17 @@ waitLoop:
terminated = true terminated = true
} }
} }
waitErr = <-waitCh for {
break waitLoop select {
case err := <-waitCh:
waitErr = err
break waitLoop
case <-time.After(forceKillWaitTimeout):
if proc := cmd.Process(); proc != nil {
_ = proc.Kill()
}
}
}
case <-messageTimerCh: case <-messageTimerCh:
forcedAfterComplete = true forcedAfterComplete = true
messageTimerCh = nil messageTimerCh = nil
@@ -1135,8 +1146,17 @@ waitLoop:
// Close pipes to unblock stream readers, then wait for process exit. // Close pipes to unblock stream readers, then wait for process exit.
closeWithReason(stdout, "terminate") closeWithReason(stdout, "terminate")
closeWithReason(stderr, "terminate") closeWithReason(stderr, "terminate")
waitErr = <-waitCh for {
break waitLoop select {
case err := <-waitCh:
waitErr = err
break waitLoop
case <-time.After(forceKillWaitTimeout):
if proc := cmd.Process(); proc != nil {
_ = proc.Kill()
}
}
}
case <-completeSeen: case <-completeSeen:
completeSeenObserved = true completeSeenObserved = true
if messageTimer != nil { if messageTimer != nil {

View File

@@ -9,6 +9,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
) )
// sendTermSignal on Windows directly kills the process. // sendTermSignal on Windows directly kills the process.
@@ -31,6 +32,56 @@ func sendTermSignal(proc processHandle) error {
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
return nil return nil
} }
if err := killProcessTree(pid); err == nil {
return nil
}
} }
return proc.Kill() return proc.Kill()
} }
func killProcessTree(pid int) error {
if pid <= 0 {
return nil
}
wmic := "wmic"
if root := os.Getenv("SystemRoot"); root != "" {
wmic = filepath.Join(root, "System32", "wbem", "WMIC.exe")
}
queryChildren := "(ParentProcessId=" + strconv.Itoa(pid) + ")"
listCmd := exec.Command(wmic, "process", "where", queryChildren, "get", "ProcessId", "/VALUE")
listCmd.Stderr = io.Discard
out, err := listCmd.Output()
if err == nil {
for _, childPID := range parseWMICPIDs(out) {
_ = killProcessTree(childPID)
}
}
querySelf := "(ProcessId=" + strconv.Itoa(pid) + ")"
termCmd := exec.Command(wmic, "process", "where", querySelf, "call", "terminate")
termCmd.Stdout = io.Discard
termCmd.Stderr = io.Discard
if termErr := termCmd.Run(); termErr != nil && err == nil {
err = termErr
}
return err
}
func parseWMICPIDs(out []byte) []int {
const prefix = "ProcessId="
var pids []int
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, prefix) {
continue
}
n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix)))
if err != nil || n <= 0 {
continue
}
pids = append(pids, n)
}
return pids
}

View File

@@ -1,6 +1,12 @@
# OmO Multi-Agent Orchestration # OmO Multi-Agent Orchestration
OmO (Oh-My-OpenCode) is a multi-agent orchestration skill that uses Sisyphus as the primary coordinator to delegate tasks to specialized agents. OmO (Oh-My-OpenCode) is a multi-agent orchestration skill that delegates tasks to specialized agents based on routing signals.
## Installation
```bash
python3 install.py --module omo
```
## Quick Start ## Quick Start
@@ -12,19 +18,17 @@ OmO (Oh-My-OpenCode) is a multi-agent orchestration skill that uses Sisyphus as
| Agent | Role | Backend | Model | | Agent | Role | Backend | Model |
|-------|------|---------|-------| |-------|------|---------|-------|
| sisyphus | Primary orchestrator | claude | claude-sonnet-4-20250514 | | oracle | Technical advisor | claude | claude-opus-4-5-20251101 |
| oracle | Technical advisor (EXPENSIVE) | claude | claude-sonnet-4-20250514 | | librarian | External research | claude | claude-sonnet-4-5-20250929 |
| librarian | External research | claude | claude-sonnet-4-5-20250514 | | explore | Codebase search | opencode | opencode/grok-code |
| explore | Codebase search (FREE) | opencode | opencode/grok-code | | develop | Code implementation | codex | gpt-5.2 |
| develop | Code implementation | codex | (default) | | frontend-ui-ux-engineer | UI/UX specialist | gemini | gemini-3-pro-high |
| frontend-ui-ux-engineer | UI/UX specialist | gemini | gemini-3-pro-preview | | document-writer | Documentation | gemini | gemini-3-flash |
| document-writer | Documentation | gemini | gemini-3-flash-preview |
## How It Works ## How It Works
1. `/omo` loads Sisyphus as the entry point 1. `/omo` analyzes your request via routing signals
2. Sisyphus analyzes your request via routing signals 2. Based on task type, it either:
3. Based on task type, Sisyphus either:
- Answers directly (analysis/explanation tasks - no code changes) - Answers directly (analysis/explanation tasks - no code changes)
- Delegates to specialized agents (implementation tasks) - Delegates to specialized agents (implementation tasks)
- Fires parallel agents (exploration + research) - Fires parallel agents (exploration + research)
@@ -44,7 +48,7 @@ OmO (Oh-My-OpenCode) is a multi-agent orchestration skill that uses Sisyphus as
## Agent Delegation ## Agent Delegation
Sisyphus delegates via codeagent-wrapper with full Context Pack: Delegates via codeagent-wrapper with full Context Pack:
```bash ```bash
codeagent-wrapper --agent oracle - . <<'EOF' codeagent-wrapper --agent oracle - . <<'EOF'
@@ -70,11 +74,43 @@ Agent-model mappings are configured in `~/.codeagent/models.json`:
```json ```json
{ {
"default_backend": "opencode", "default_backend": "codex",
"default_model": "opencode/grok-code", "default_model": "gpt-5.2",
"agents": { "agents": {
"sisyphus": {"backend": "claude", "model": "claude-sonnet-4-20250514"}, "oracle": {
"oracle": {"backend": "claude", "model": "claude-sonnet-4-20250514"} "backend": "claude",
"model": "claude-opus-4-5-20251101",
"description": "Technical advisor",
"yolo": true
},
"librarian": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929",
"description": "Researcher",
"yolo": true
},
"explore": {
"backend": "opencode",
"model": "opencode/grok-code",
"description": "Code search"
},
"frontend-ui-ux-engineer": {
"backend": "gemini",
"model": "gemini-3-pro-high",
"description": "Frontend engineer"
},
"document-writer": {
"backend": "gemini",
"model": "gemini-3-flash",
"description": "Documentation"
},
"develop": {
"backend": "codex",
"model": "gpt-5.2",
"description": "codex develop",
"yolo": true,
"reasoning": "xhigh"
}
} }
} }
``` ```
@@ -82,4 +118,4 @@ Agent-model mappings are configured in `~/.codeagent/models.json`:
## Requirements ## Requirements
- codeagent-wrapper with `--agent` support - codeagent-wrapper with `--agent` support
- Backend CLIs: claude, opencode, gemini - Backend CLIs: claude, opencode, codex, gemini