Files
patterm/internal/harness/scenarios/timer_idle_any_fires_on_transition.json
Harry Bayliss 2b9e1ed77c Add idle-state classifier and Solo-parity timer tools
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.
2026-05-15 09:49:59 +01:00

68 lines
1.7 KiB
JSON

{
"name": "timer_idle_any_fires_on_transition",
"presets": {
"processes": [
{
"name": "echoer",
"argv": ["sh", "-lc", "while read line; do echo \"saw:$line\"; done"]
},
{
"name": "busy",
"argv": ["sh", "-lc", "for i in 1 2 3 4 5; do echo tick $i; sleep 0.2; done; sleep 60"],
"idle_detection": {
"strategy": "output_activity",
"idle_threshold_ms": 500
}
}
]
},
"trust": ["echoer", "busy"],
"steps": [
{
"type": "mcp_call",
"method": "spawn_process",
"params": {"kind": "command", "preset": "echoer", "name": "echoer"},
"save_as": "owner"
},
{
"type": "mcp_call",
"method": "spawn_process",
"params": {"kind": "command", "preset": "busy", "name": "busy"},
"save_as": "watch"
},
{
"type": "wait_until_mcp",
"method": "get_process_status",
"params": {"process_id": "{{watch.process_id}}"},
"path": "idle_state",
"equals": "working",
"timeout_ms": 3000
},
{
"type": "mcp_call",
"method": "timer_fire_when_idle_any",
"params": {
"watched": ["{{watch.process_id}}"],
"body": "any-idle",
"owner_process_id": "{{owner.process_id}}"
},
"save_as": "resp"
},
{
"type": "assert_saved",
"from": "resp",
"path": "status",
"equals": "pending"
},
{
"type": "wait_until_mcp",
"method": "get_process_output",
"params": {"process_id": "{{owner.process_id}}", "mode": "grid"},
"path": "content",
"contains": "saw:any-idle",
"allow_substring": true,
"timeout_ms": 6000
}
]
}