fix(tmux): add debounce delay between paste and Enter

Fixes race condition where Enter key arrives before paste is fully
processed, causing workers to sit idle at prompts.

- SendKeys now uses 100ms default debounce
- New SendKeysDebounced allows configurable delay
- Inject scales delay 100-500ms based on message size

Closes: gt-w3bu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-18 21:08:23 -08:00
parent bc8e7e66ea
commit 1229b98bdf
3 changed files with 22 additions and 1 deletions

View File

@@ -256,6 +256,7 @@
{"id":"gt-vci","title":"Mayor handoff mail template","description":"Add MAYOR_HANDOFF mail template to templates.py.\n\n## Template Function\n\ndef mayor_handoff(\n active_swarms: List[SwarmStatus],\n rig_status: Dict[str, RigStatus],\n pending_escalations: List[Escalation],\n in_flight_decisions: List[Decision],\n recent_actions: List[str],\n delegated_work: List[DelegatedItem],\n user_requests: List[str],\n next_steps: List[str],\n warnings: Optional[str] = None,\n session_duration: Optional[str] = None,\n) -\u003e Message:\n metadata = {\n 'template': 'MAYOR_HANDOFF',\n 'timestamp': datetime.utcnow().isoformat(),\n 'session_duration': session_duration,\n 'active_swarm_count': len(active_swarms),\n 'pending_escalation_count': len(pending_escalations),\n }\n # ... format sections ...\n return Message.create(\n sender='mayor/',\n recipient='mayor/',\n subject='Session Handoff',\n body=body,\n priority='high',\n )\n\n## Metadata Fields\n\n- template: MAYOR_HANDOFF\n- timestamp: ISO format\n- session_duration: Human readable\n- active_swarm_count: Number of active swarms\n- pending_escalation_count: Number of escalations\n\n## Mail Priority\n\nUse priority='high' to ensure handoff is seen on startup.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T20:15:30.26323-08:00","updated_at":"2025-12-15T20:48:59.550689-08:00","dependencies":[{"issue_id":"gt-vci","depends_on_id":"gt-u82","type":"blocks","created_at":"2025-12-15T20:15:39.554108-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"gt-vjv","title":"Add bulk session stop command (gt session stop --all)","description":"When decommissioning a rig, need to stop multiple sessions one at a time. A --all or --rig flag would allow: gt session stop --rig gastown","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-18T11:33:33.394649-08:00","updated_at":"2025-12-18T11:38:51.399298-08:00","closed_at":"2025-12-18T11:38:51.399298-08:00","close_reason":"Wrong model: streaming doesn't need bulk shutdown commands - polecats request their own shutdown"}
{"id":"gt-vjw","title":"Swarm learning: Session cleanup missing from swarm workflow","description":"## Problem\n\nAfter Enders Game swarm completed (18 issues merged), 16 polecat sessions were left running but idle. No automated cleanup occurred.\n\n## What Should Happen\n\n1. Witness detects polecat completed work (idle at prompt)\n2. Witness verifies git state is clean\n3. Witness shuts down session\n4. Witness reports completion to Mayor\n\n## GGT Components\n\n- gt-cxx: Witness context cycling (covers self-cycling)\n- gt-u1j.9: Witness daemon heartbeat loop\n- gt-kmn.6: Witness swarm landing protocol\n\n## Recommendation\n\nAdd to Witness responsibilities:\n- Monitor for 'work complete' signals (DONE keyword, idle detection)\n- Automated session shutdown after verification\n- Swarm completion reporting to Mayor\n\nSee also: architecture.md 'Worker Cleanup (Witness-Owned)' section which describes this but it wasn't implemented in PGT.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-16T01:27:52.796587-08:00","updated_at":"2025-12-16T01:51:37.409965-08:00","closed_at":"2025-12-16T01:51:37.409965-08:00","close_reason":"Addressed by gt-u1j.9 (daemon heartbeat) which now includes session lifecycle management and worker shutdown. See architecture.md Key Decision #12."}
{"id":"gt-w3bu","title":"gt spawn: Enter key needs debounce delay after paste","description":"## Problem\n\nWhen spawning polecats via `gt spawn`, instructions are pasted into tmux but workers often sit idle at prompt because the Enter key arrives before the paste is fully processed.\n\n## Observed Behavior\n\n- Instructions pasted via tmux send-keys\n- Enter key sent immediately after\n- Worker session shows prompt with no input (paste not submitted)\n- Requires manual intervention to nudge workers\n\n## Expected Behavior\n\n- Paste completes fully\n- Enter key submits the pasted instructions\n- Worker begins executing immediately\n\n## Root Cause\n\nLikely a race condition between paste buffer processing and keypress handling. Need either:\n1. Debounce delay before sending Enter\n2. Longer delay (Tmax) to ensure paste completes\n3. Alternative submission mechanism\n\n## Impact\n\n- Swarm of 10 workers had multiple stalls at startup\n- Required manual tmux send-keys to unstick them\n- Defeats purpose of automated spawning\n\n## References\n\n- Observed during Mad Max swarm (Dec 18, 2025)\n- Affects gt spawn command in internal/cmd/spawn.go","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-18T21:06:50.734123-08:00","updated_at":"2025-12-18T21:08:22.400899-08:00","closed_at":"2025-12-18T21:08:22.400899-08:00","close_reason":"Added debounce delay between paste and Enter in SendKeys. Default 100ms, Inject scales 100-500ms based on message size."}
{"id":"gt-w775","title":"MR: gt-svi.1 (polecat/Furiosa)","description":"branch: polecat/Furiosa\ntarget: main\nsource_issue: gt-svi.1","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-18T20:21:40.921429-08:00","updated_at":"2025-12-18T20:21:54.163532-08:00","closed_at":"2025-12-18T20:21:54.163532-08:00","close_reason":"Test MRs, cleaning up"}
{"id":"gt-w9o","title":"/restart: Personal slash command for in-place agent restart","description":"Create ~/.claude/commands/restart.md that restarts current Gas Town agent in place.\n\n## Detection\n- Read tmux session name: gt-mayor, gt-witness-*, gt-refinery-*, gt-polecat-*\n- Fallback: check GT_ROLE env var\n\n## Behavior by role\n- mayor: gt mayor restart (sends Ctrl-C, loop respawns)\n- witness: gt witness restart\n- refinery: gt refinery restart \n- polecat: gt polecat restart (or witness-mediated)\n\n## Command format\nUses backticks for inline bash to detect context, then instructs Claude to run appropriate restart.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T18:32:30.043125-08:00","updated_at":"2025-12-18T18:43:17.182303-08:00","closed_at":"2025-12-18T18:43:17.182303-08:00","close_reason":"Implemented ~/.claude/commands/restart.md"}
{"id":"gt-wpg","title":"Replaceable notifications via Claude Code queue","description":"Leverage Claude Code's ability to replace queued text for notifications that supersede previous ones.\n\n## Problem\n\nIf daemon sends 10 heartbeats while agent is busy, agent returns to see 10 stacked messages. Wasteful and noisy.\n\n## Solution\n\nUse Claude Code's queue replacement for:\n- Heartbeat messages (only latest matters)\n- Status updates that supersede previous\n- Progress notifications\n\n## Implementation\n\nNotifications get a 'slot' identifier. New notification in same slot replaces old one:\n- Slot: 'heartbeat' → only one heartbeat queued at a time\n- Slot: 'status-\u003crig\u003e' → latest status per rig\n- No slot → stacks normally (for unique messages)\n\n## Research Needed\n\n- How does Claude Code expose queue replacement?\n- tmux send-keys behavior with pending input\n- Alternative: clear + resend pattern","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-18T14:19:29.821949-08:00","updated_at":"2025-12-18T14:19:29.821949-08:00","dependencies":[{"issue_id":"gt-wpg","depends_on_id":"gt-99m","type":"blocks","created_at":"2025-12-18T14:19:46.656972-08:00","created_by":"daemon"}]}

View File

@@ -228,6 +228,7 @@ func (m *Manager) Capture(polecat string, lines int) (string, error) {
}
// Inject sends a message to a polecat session.
// Uses a longer debounce delay for large messages to ensure paste completes.
func (m *Manager) Inject(polecat, message string) error {
sessionID := m.sessionName(polecat)
@@ -239,7 +240,14 @@ func (m *Manager) Inject(polecat, message string) error {
return ErrSessionNotFound
}
return m.tmux.SendKeys(sessionID, message)
// Use longer debounce for large messages (spawn context can be 1KB+)
// Scale delay based on message size: 100ms base + 50ms per KB
debounceMs := 100 + (len(message)/1024)*50
if debounceMs > 500 {
debounceMs = 500 // Cap at 500ms
}
return m.tmux.SendKeysDebounced(sessionID, message, debounceMs)
}
// StopAll terminates all sessions for this rig.

View File

@@ -110,11 +110,23 @@ func (t *Tmux) ListSessions() ([]string, error) {
// SendKeys sends keystrokes to a session and presses Enter.
// Always sends Enter as a separate command for reliability.
// Uses a debounce delay between paste and Enter to ensure paste completes.
func (t *Tmux) SendKeys(session, keys string) error {
return t.SendKeysDebounced(session, keys, 100) // 100ms default debounce
}
// SendKeysDebounced sends keystrokes with a configurable delay before Enter.
// The debounceMs parameter controls how long to wait after paste before sending Enter.
// This prevents race conditions where Enter arrives before paste is processed.
func (t *Tmux) SendKeysDebounced(session, keys string, debounceMs int) error {
// Send text using literal mode (-l) to handle special chars
if _, err := t.run("send-keys", "-t", session, "-l", keys); err != nil {
return err
}
// Wait for paste to be processed
if debounceMs > 0 {
time.Sleep(time.Duration(debounceMs) * time.Millisecond)
}
// Send Enter separately - more reliable than appending to send-keys
_, err := t.run("send-keys", "-t", session, "Enter")
return err