Classifies every running child as idle/working/thinking/permission/error using one of three pluggable strategies (output_activity, osc_title_stability, osc_title_status) plus optional regex promoters applied to the tail of recent output. State and last-match reason are exposed via MCP on ProcessInfo and get_process_status. Per-preset configuration lives on a new preset.IdleDetection block with bundled defaults for the first-party claude/codex/opencode presets. OSC title plumbing is exposed as Emulator.Title(), polled from the session pump after each emulator write so title-change activity feeds into the classifier without an extra cgo callback. The MCP timer surface expands to match Solo: timer_set, timer_fire_when_idle_any/all, timer_cancel, timer_pause, timer_resume, timer_list. timer_wait is now a thin wrapper that shares the same manager so it shows up in timer_list while pending. Timer bodies are delivered to the owner process through the existing InjectAsOrchestrator path. Top-level (non-agent) callers can attach timers to a specific process via owner_process_id; omitting it grants universal cancel/pause/resume/list privileges. The sidebar gains a state glyph per process row and appends a nearest-timer indicator when one is pending or paused. Tests: idle_test.go covers the classify() pure function across the three strategies and regex promotion; timers_test.go covers the manager. Harness scenarios cover output_activity, osc_title_stability, osc_title_status, and regex promotion, plus timer_set delivery, cancel, pause/resume, idle_any-on-transition, idle_all-pending, and idle_all-already-satisfied. A new wait_until_mcp harness step type polls an MCP method until an assertion holds.
63 lines
1.4 KiB
JSON
63 lines
1.4 KiB
JSON
{
|
|
"name": "timer_pause_resume",
|
|
"presets": {
|
|
"processes": [
|
|
{
|
|
"name": "echoer",
|
|
"argv": ["sh", "-lc", "while read line; do echo \"saw:$line\"; done"]
|
|
}
|
|
]
|
|
},
|
|
"trust": ["echoer"],
|
|
"steps": [
|
|
{
|
|
"type": "mcp_call",
|
|
"method": "spawn_process",
|
|
"params": {"kind": "command", "preset": "echoer", "name": "echoer"},
|
|
"save_as": "proc"
|
|
},
|
|
{ "type": "wait_stable", "timeout_ms": 1500 },
|
|
{
|
|
"type": "mcp_call",
|
|
"method": "timer_set",
|
|
"params": {
|
|
"seconds": 1,
|
|
"body": "after-resume",
|
|
"owner_process_id": "{{proc.process_id}}"
|
|
},
|
|
"save_as": "tmr"
|
|
},
|
|
{
|
|
"type": "mcp_call",
|
|
"method": "timer_pause",
|
|
"params": {"timer_id": "{{tmr.timer_id}}"}
|
|
},
|
|
{
|
|
"type": "mcp_call",
|
|
"method": "timer_list",
|
|
"params": {"owner_process_id": "{{proc.process_id}}"},
|
|
"save_as": "listed"
|
|
},
|
|
{
|
|
"type": "assert_saved",
|
|
"from": "listed",
|
|
"path": "0.status",
|
|
"equals": "paused"
|
|
},
|
|
{
|
|
"type": "mcp_call",
|
|
"method": "timer_resume",
|
|
"params": {"timer_id": "{{tmr.timer_id}}"}
|
|
},
|
|
{
|
|
"type": "wait_until_mcp",
|
|
"method": "get_process_output",
|
|
"params": {"process_id": "{{proc.process_id}}", "mode": "grid"},
|
|
"path": "content",
|
|
"contains": "saw:after-resume",
|
|
"allow_substring": true,
|
|
"timeout_ms": 5000
|
|
}
|
|
]
|
|
}
|