aicodeman 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/ai-checker-base.d.ts.map +1 -1
  2. package/dist/ai-checker-base.js +3 -2
  3. package/dist/ai-checker-base.js.map +1 -1
  4. package/dist/bash-tool-parser.d.ts +6 -0
  5. package/dist/bash-tool-parser.d.ts.map +1 -1
  6. package/dist/bash-tool-parser.js +87 -101
  7. package/dist/bash-tool-parser.js.map +1 -1
  8. package/dist/file-stream-manager.d.ts.map +1 -1
  9. package/dist/file-stream-manager.js +2 -1
  10. package/dist/file-stream-manager.js.map +1 -1
  11. package/dist/hooks-config.d.ts +5 -0
  12. package/dist/hooks-config.d.ts.map +1 -1
  13. package/dist/hooks-config.js +25 -0
  14. package/dist/hooks-config.js.map +1 -1
  15. package/dist/orchestrator-loop.d.ts +2 -0
  16. package/dist/orchestrator-loop.d.ts.map +1 -1
  17. package/dist/orchestrator-loop.js +27 -22
  18. package/dist/orchestrator-loop.js.map +1 -1
  19. package/dist/orchestrator-verifier.d.ts +1 -1
  20. package/dist/orchestrator-verifier.d.ts.map +1 -1
  21. package/dist/orchestrator-verifier.js +3 -2
  22. package/dist/orchestrator-verifier.js.map +1 -1
  23. package/dist/plan-orchestrator.d.ts +4 -1
  24. package/dist/plan-orchestrator.d.ts.map +1 -1
  25. package/dist/plan-orchestrator.js +66 -88
  26. package/dist/plan-orchestrator.js.map +1 -1
  27. package/dist/ralph-status-parser.d.ts +2 -0
  28. package/dist/ralph-status-parser.d.ts.map +1 -1
  29. package/dist/ralph-status-parser.js +98 -102
  30. package/dist/ralph-status-parser.js.map +1 -1
  31. package/dist/respawn-controller.d.ts +4 -1
  32. package/dist/respawn-controller.d.ts.map +1 -1
  33. package/dist/respawn-controller.js +165 -131
  34. package/dist/respawn-controller.js.map +1 -1
  35. package/dist/session.d.ts +18 -0
  36. package/dist/session.d.ts.map +1 -1
  37. package/dist/session.js +129 -117
  38. package/dist/session.js.map +1 -1
  39. package/dist/state-store.d.ts +2 -0
  40. package/dist/state-store.d.ts.map +1 -1
  41. package/dist/state-store.js +22 -28
  42. package/dist/state-store.js.map +1 -1
  43. package/dist/subagent-watcher.d.ts +6 -0
  44. package/dist/subagent-watcher.d.ts.map +1 -1
  45. package/dist/subagent-watcher.js +145 -139
  46. package/dist/subagent-watcher.js.map +1 -1
  47. package/dist/tunnel-manager.d.ts.map +1 -1
  48. package/dist/tunnel-manager.js +2 -1
  49. package/dist/tunnel-manager.js.map +1 -1
  50. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  51. package/dist/web/public/{app.3b7ff137.js → app.16290ae3.js} +2 -2
  52. package/dist/web/public/app.16290ae3.js.br +0 -0
  53. package/dist/web/public/app.16290ae3.js.gz +0 -0
  54. package/dist/web/public/constants.64161167.js.gz +0 -0
  55. package/dist/web/public/index.html +26 -8
  56. package/dist/web/public/index.html.br +0 -0
  57. package/dist/web/public/index.html.gz +0 -0
  58. package/dist/web/public/input-cjk.88082175.js.gz +0 -0
  59. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  60. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  61. package/dist/web/public/mobile.9a61290c.css +1 -0
  62. package/dist/web/public/mobile.9a61290c.css.br +0 -0
  63. package/dist/web/public/mobile.9a61290c.css.gz +0 -0
  64. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  65. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  66. package/dist/web/public/{panels-ui.8204db1e.js → panels-ui.2d5b9703.js} +1 -1
  67. package/dist/web/public/panels-ui.2d5b9703.js.br +0 -0
  68. package/dist/web/public/panels-ui.2d5b9703.js.gz +0 -0
  69. package/dist/web/public/{ralph-panel.a2733fd5.js → ralph-panel.61076370.js} +1 -1
  70. package/dist/web/public/ralph-panel.61076370.js.br +0 -0
  71. package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
  72. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  73. package/dist/web/public/{respawn-ui.372c6ea7.js → respawn-ui.60be6ef5.js} +1 -1
  74. package/dist/web/public/respawn-ui.60be6ef5.js.br +0 -0
  75. package/dist/web/public/respawn-ui.60be6ef5.js.gz +0 -0
  76. package/dist/web/public/session-ui.ab189b7f.js +16 -0
  77. package/dist/web/public/session-ui.ab189b7f.js.br +0 -0
  78. package/dist/web/public/session-ui.ab189b7f.js.gz +0 -0
  79. package/dist/web/public/settings-ui.50a8018e.js +55 -0
  80. package/dist/web/public/settings-ui.50a8018e.js.br +0 -0
  81. package/dist/web/public/settings-ui.50a8018e.js.gz +0 -0
  82. package/dist/web/public/styles.111ff326.css.gz +0 -0
  83. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  84. package/dist/web/public/sw.js.gz +0 -0
  85. package/dist/web/public/terminal-ui.474f79df.js.gz +0 -0
  86. package/dist/web/public/upload.html.gz +0 -0
  87. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  88. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  89. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  90. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  91. package/dist/web/public/vendor/xterm.css.gz +0 -0
  92. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  93. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  94. package/dist/web/respawn-event-wiring.d.ts +51 -0
  95. package/dist/web/respawn-event-wiring.d.ts.map +1 -0
  96. package/dist/web/respawn-event-wiring.js +280 -0
  97. package/dist/web/respawn-event-wiring.js.map +1 -0
  98. package/dist/web/route-helpers.d.ts +9 -0
  99. package/dist/web/route-helpers.d.ts.map +1 -1
  100. package/dist/web/route-helpers.js +15 -0
  101. package/dist/web/route-helpers.js.map +1 -1
  102. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  103. package/dist/web/routes/orchestrator-routes.js +23 -30
  104. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  105. package/dist/web/routes/session-routes.d.ts.map +1 -1
  106. package/dist/web/routes/session-routes.js +5 -1
  107. package/dist/web/routes/session-routes.js.map +1 -1
  108. package/dist/web/routes/system-routes.d.ts.map +1 -1
  109. package/dist/web/routes/system-routes.js +12 -30
  110. package/dist/web/routes/system-routes.js.map +1 -1
  111. package/dist/web/schemas.d.ts +1 -0
  112. package/dist/web/schemas.d.ts.map +1 -1
  113. package/dist/web/schemas.js +2 -0
  114. package/dist/web/schemas.js.map +1 -1
  115. package/dist/web/server.d.ts +4 -51
  116. package/dist/web/server.d.ts.map +1 -1
  117. package/dist/web/server.js +98 -941
  118. package/dist/web/server.js.map +1 -1
  119. package/dist/web/session-listener-wiring.d.ts +89 -0
  120. package/dist/web/session-listener-wiring.d.ts.map +1 -0
  121. package/dist/web/session-listener-wiring.js +290 -0
  122. package/dist/web/session-listener-wiring.js.map +1 -0
  123. package/dist/web/sse-stream-manager.d.ts +91 -0
  124. package/dist/web/sse-stream-manager.d.ts.map +1 -0
  125. package/dist/web/sse-stream-manager.js +426 -0
  126. package/dist/web/sse-stream-manager.js.map +1 -0
  127. package/package.json +1 -1
  128. package/dist/web/public/app.3b7ff137.js.br +0 -0
  129. package/dist/web/public/app.3b7ff137.js.gz +0 -0
  130. package/dist/web/public/mobile.0b213796.css +0 -1
  131. package/dist/web/public/mobile.0b213796.css.br +0 -0
  132. package/dist/web/public/mobile.0b213796.css.gz +0 -0
  133. package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
  134. package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
  135. package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
  136. package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
  137. package/dist/web/public/respawn-ui.372c6ea7.js.br +0 -0
  138. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  139. package/dist/web/public/session-ui.554092ae.js +0 -16
  140. package/dist/web/public/session-ui.554092ae.js.br +0 -0
  141. package/dist/web/public/session-ui.554092ae.js.gz +0 -0
  142. package/dist/web/public/settings-ui.bd3eaadb.js +0 -55
  143. package/dist/web/public/settings-ui.bd3eaadb.js.br +0 -0
  144. package/dist/web/public/settings-ui.bd3eaadb.js.gz +0 -0
@@ -53,6 +53,7 @@ import { RespawnAdaptiveTiming } from './respawn-adaptive-timing.js';
53
53
  import { RespawnCycleMetricsTracker } from './respawn-metrics.js';
54
54
  import { calculateHealthScore, shouldSkipClear } from './respawn-health.js';
55
55
  import { AI_CHECK_MODEL, AI_IDLE_CHECK_MAX_CONTEXT, AI_PLAN_CHECK_MAX_CONTEXT, AI_IDLE_CHECK_TIMEOUT_MS, AI_IDLE_CHECK_COOLDOWN_MS, AI_PLAN_CHECK_TIMEOUT_MS, AI_PLAN_CHECK_COOLDOWN_MS, } from './config/ai-defaults.js';
56
+ import { getErrorMessage, } from './types.js';
56
57
  // ========== Constants ==========
57
58
  // COMPLETION_TIME_PATTERN moved to ./respawn-patterns.ts
58
59
  /** Pre-filter: numbered option pattern for plan mode detection */
@@ -310,24 +311,27 @@ export class RespawnController extends EventEmitter {
310
311
  c[field] = DEFAULT_CONFIG[field];
311
312
  }
312
313
  };
313
- // Ensure timeouts are positive
314
- validatePositiveTimeout('idleTimeoutMs');
315
- validatePositiveTimeout('completionConfirmMs');
316
- validatePositiveTimeout('noOutputTimeoutMs');
317
- validatePositiveTimeout('autoAcceptDelayMs', true);
318
- validatePositiveTimeout('interStepDelayMs');
314
+ const REQUIRED_TIMEOUT_FIELDS = [
315
+ 'idleTimeoutMs',
316
+ 'completionConfirmMs',
317
+ 'noOutputTimeoutMs',
318
+ 'interStepDelayMs',
319
+ 'aiIdleCheckTimeoutMs',
320
+ 'aiIdleCheckMaxContext',
321
+ 'aiPlanCheckTimeoutMs',
322
+ 'aiPlanCheckMaxContext',
323
+ ];
324
+ for (const field of REQUIRED_TIMEOUT_FIELDS) {
325
+ validatePositiveTimeout(field);
326
+ }
327
+ const ALLOW_ZERO_FIELDS = ['autoAcceptDelayMs', 'aiIdleCheckCooldownMs', 'aiPlanCheckCooldownMs'];
328
+ for (const field of ALLOW_ZERO_FIELDS) {
329
+ validatePositiveTimeout(field, true);
330
+ }
319
331
  // Ensure completion confirm doesn't exceed no-output timeout
320
332
  if (c.completionConfirmMs > c.noOutputTimeoutMs) {
321
333
  c.completionConfirmMs = c.noOutputTimeoutMs;
322
334
  }
323
- // Ensure AI check timeouts are positive
324
- validatePositiveTimeout('aiIdleCheckTimeoutMs');
325
- validatePositiveTimeout('aiIdleCheckCooldownMs', true);
326
- validatePositiveTimeout('aiIdleCheckMaxContext');
327
- // Ensure plan check timeouts are positive
328
- validatePositiveTimeout('aiPlanCheckTimeoutMs');
329
- validatePositiveTimeout('aiPlanCheckCooldownMs', true);
330
- validatePositiveTimeout('aiPlanCheckMaxContext');
331
335
  }
332
336
  /** Wire up AI checker events to controller events (removes existing listeners first to prevent duplicates) */
333
337
  setupAiCheckerListeners() {
@@ -796,88 +800,13 @@ export class RespawnController extends EventEmitter {
796
800
  this.lastTokenCount = tokenCount;
797
801
  this.lastTokenChangeTime = now;
798
802
  }
799
- // Detect completion message FIRST (Layer 1) - PRIMARY DETECTION
800
- // Check this before working patterns because completion message indicates
801
- // the work is done, even if working patterns are still in the rolling window
802
- if (isCompletionMessage(data)) {
803
- // Clear the rolling window - completion marks a transition point
804
- this.clearWorkingPatternWindow();
805
- this.workingDetected = false;
806
- this.completionMessageTime = now;
807
- this.cancelAutoAcceptTimer(); // Normal idle flow handles this
808
- this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
809
- // In watching state, start completion confirmation timer
810
- if (this._state === 'watching') {
811
- this.startCompletionConfirmTimer();
812
- return;
813
- }
814
- // In waiting states, also use confirmation timer (same detection logic)
815
- // This ensures we wait for Claude to finish before proceeding
816
- // Note: 'watching' is already handled above and returns early
817
- switch (this._state) {
818
- case 'waiting_update':
819
- this.startStepConfirmTimer('update');
820
- break;
821
- case 'waiting_clear':
822
- this.checkClearComplete(); // /clear is quick, no need to wait
823
- break;
824
- case 'waiting_init':
825
- this.startStepConfirmTimer('init');
826
- break;
827
- case 'waiting_kickstart':
828
- this.startStepConfirmTimer('kickstart');
829
- break;
830
- // Non-waiting states: completion message is ignored
831
- case 'confirming_idle':
832
- case 'ai_checking':
833
- case 'sending_update':
834
- case 'sending_clear':
835
- case 'sending_init':
836
- case 'monitoring_init':
837
- case 'sending_kickstart':
838
- case 'stopped':
839
- // Completion message during these states is ignored
840
- break;
841
- default:
842
- assertNever(this._state, `Unhandled RespawnState in completion detection: ${this._state}`);
843
- }
803
+ // Layer 1: Completion message (PRIMARY) checked before working patterns
804
+ if (this._detectCompletionMessage(data, now))
844
805
  return;
845
- }
846
- // Detect working patterns (Layer 4)
847
- const isWorking = this.checkWorkingPattern(data);
848
- if (isWorking) {
849
- this.workingDetected = true;
850
- this.promptDetected = false;
851
- this.elicitationDetected = false; // Clear on new work cycle
852
- this.resetHookState(); // Clear hook signals on new work
853
- this.lastWorkingPatternTime = now;
854
- // Cancel hook confirmation timer if running
855
- this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
856
- // Cancel any pending completion confirmation
857
- this.cancelCompletionConfirm();
858
- // Cancel any pending step confirmation (Claude is still working)
859
- this.cancelStepConfirm();
860
- // If AI check is running, cancel it (Claude is working)
861
- if (this._state === 'ai_checking') {
862
- this.log('Working patterns detected during AI check, cancelling');
863
- this.aiChecker.cancel();
864
- this.setState('watching');
865
- }
866
- // Cancel plan check if running (Claude started working)
867
- if (this.planChecker.status === 'checking') {
868
- this.log('Working patterns detected during plan check, cancelling');
869
- this.planChecker.cancel();
870
- }
871
- // If we're monitoring init and work started, go to watching (no kickstart needed)
872
- if (this._state === 'monitoring_init') {
873
- this.log('/init triggered work, skipping kickstart');
874
- this.emit('stepCompleted', 'init');
875
- this.completeCycle();
876
- }
806
+ // Layer 4: Working patterns
807
+ if (this._detectWorkingPattern(data, now))
877
808
  return;
878
- }
879
- // In confirming_idle or ai_checking state, substantial output cancels the flow.
880
- // This prevents false triggers when Claude pauses briefly mid-work.
809
+ // Substantial output during confirming_idle/ai_checking cancels the flow
881
810
  if (this._state === 'confirming_idle' || this._state === 'ai_checking') {
882
811
  // Strip ANSI escape codes to check if there's real content
883
812
  ANSI_ESCAPE_PATTERN_SIMPLE.lastIndex = 0;
@@ -896,42 +825,125 @@ export class RespawnController extends EventEmitter {
896
825
  return;
897
826
  }
898
827
  }
899
- // Legacy fallback: detect prompt characters (still useful for waiting_* states)
828
+ // Legacy fallback: prompt detection
829
+ this._detectPrompt(data);
830
+ }
831
+ _detectCompletionMessage(data, now) {
832
+ if (!isCompletionMessage(data))
833
+ return false;
834
+ // Clear the rolling window - completion marks a transition point
835
+ this.clearWorkingPatternWindow();
836
+ this.workingDetected = false;
837
+ this.completionMessageTime = now;
838
+ this.cancelAutoAcceptTimer(); // Normal idle flow handles this
839
+ this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
840
+ // In watching state, start completion confirmation timer
841
+ if (this._state === 'watching') {
842
+ this.startCompletionConfirmTimer();
843
+ return true;
844
+ }
845
+ // In waiting states, also use confirmation timer (same detection logic)
846
+ // This ensures we wait for Claude to finish before proceeding
847
+ // Note: 'watching' is already handled above and returns early
848
+ switch (this._state) {
849
+ case 'waiting_update':
850
+ this.startStepConfirmTimer('update');
851
+ break;
852
+ case 'waiting_clear':
853
+ this.checkClearComplete(); // /clear is quick, no need to wait
854
+ break;
855
+ case 'waiting_init':
856
+ this.startStepConfirmTimer('init');
857
+ break;
858
+ case 'waiting_kickstart':
859
+ this.startStepConfirmTimer('kickstart');
860
+ break;
861
+ // Non-waiting states: completion message is ignored
862
+ case 'confirming_idle':
863
+ case 'ai_checking':
864
+ case 'sending_update':
865
+ case 'sending_clear':
866
+ case 'sending_init':
867
+ case 'monitoring_init':
868
+ case 'sending_kickstart':
869
+ case 'stopped':
870
+ // Completion message during these states is ignored
871
+ break;
872
+ default:
873
+ assertNever(this._state, `Unhandled RespawnState in completion detection: ${this._state}`);
874
+ }
875
+ return true;
876
+ }
877
+ _detectWorkingPattern(data, now) {
878
+ const isWorking = this.checkWorkingPattern(data);
879
+ if (!isWorking)
880
+ return false;
881
+ this.workingDetected = true;
882
+ this.promptDetected = false;
883
+ this.elicitationDetected = false; // Clear on new work cycle
884
+ this.resetHookState(); // Clear hook signals on new work
885
+ this.lastWorkingPatternTime = now;
886
+ // Cancel hook confirmation timer if running
887
+ this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
888
+ // Cancel any pending completion confirmation
889
+ this.cancelCompletionConfirm();
890
+ // Cancel any pending step confirmation (Claude is still working)
891
+ this.cancelStepConfirm();
892
+ // If AI check is running, cancel it (Claude is working)
893
+ if (this._state === 'ai_checking') {
894
+ this.log('Working patterns detected during AI check, cancelling');
895
+ this.aiChecker.cancel();
896
+ this.setState('watching');
897
+ }
898
+ // Cancel plan check if running (Claude started working)
899
+ if (this.planChecker.status === 'checking') {
900
+ this.log('Working patterns detected during plan check, cancelling');
901
+ this.planChecker.cancel();
902
+ }
903
+ // If we're monitoring init and work started, go to watching (no kickstart needed)
904
+ if (this._state === 'monitoring_init') {
905
+ this.log('/init triggered work, skipping kickstart');
906
+ this.emit('stepCompleted', 'init');
907
+ this.completeCycle();
908
+ }
909
+ return true;
910
+ }
911
+ _detectPrompt(data) {
900
912
  const hasPrompt = PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
901
- if (hasPrompt) {
902
- this.promptDetected = true;
903
- this.workingDetected = false;
904
- // Handle legacy detection in waiting states - also use confirmation timers
905
- switch (this._state) {
906
- case 'waiting_update':
907
- this.startStepConfirmTimer('update');
908
- break;
909
- case 'waiting_clear':
910
- this.checkClearComplete(); // /clear is quick, no need to wait
911
- break;
912
- case 'waiting_init':
913
- this.startStepConfirmTimer('init');
914
- break;
915
- case 'monitoring_init':
916
- this.checkMonitoringInitIdle();
917
- break;
918
- case 'waiting_kickstart':
919
- this.startStepConfirmTimer('kickstart');
920
- break;
921
- // Non-waiting states: prompt detection is informational only
922
- case 'watching':
923
- case 'confirming_idle':
924
- case 'ai_checking':
925
- case 'sending_update':
926
- case 'sending_clear':
927
- case 'sending_init':
928
- case 'sending_kickstart':
929
- case 'stopped':
930
- // Prompt detection during these states doesn't trigger action
931
- break;
932
- default:
933
- assertNever(this._state, `Unhandled RespawnState in prompt detection: ${this._state}`);
934
- }
913
+ if (!hasPrompt)
914
+ return;
915
+ this.promptDetected = true;
916
+ this.workingDetected = false;
917
+ // Handle legacy detection in waiting states - also use confirmation timers
918
+ switch (this._state) {
919
+ case 'waiting_update':
920
+ this.startStepConfirmTimer('update');
921
+ break;
922
+ case 'waiting_clear':
923
+ this.checkClearComplete(); // /clear is quick, no need to wait
924
+ break;
925
+ case 'waiting_init':
926
+ this.startStepConfirmTimer('init');
927
+ break;
928
+ case 'monitoring_init':
929
+ this.checkMonitoringInitIdle();
930
+ break;
931
+ case 'waiting_kickstart':
932
+ this.startStepConfirmTimer('kickstart');
933
+ break;
934
+ // Non-waiting states: prompt detection is informational only
935
+ case 'watching':
936
+ case 'confirming_idle':
937
+ case 'ai_checking':
938
+ case 'sending_update':
939
+ case 'sending_clear':
940
+ case 'sending_init':
941
+ case 'sending_kickstart':
942
+ case 'stopped':
943
+ // Prompt detection during these states doesn't trigger action
944
+ break;
945
+ default:
946
+ assertNever(this._state, `Unhandled RespawnState in prompt detection: ${this._state}`);
935
947
  }
936
948
  }
937
949
  /**
@@ -1375,6 +1387,17 @@ export class RespawnController extends EventEmitter {
1375
1387
  this.logAction('detection', 'Skipped AI check: Session is working');
1376
1388
  return;
1377
1389
  }
1390
+ // Check for active child processes (bash tools, test suites, builds, etc.)
1391
+ // These may produce no terminal output, so restart timers to retry periodically.
1392
+ const activeProcesses = this.session.getActiveChildProcesses();
1393
+ if (activeProcesses.length > 0) {
1394
+ const names = activeProcesses.map((p) => p.command).join(', ');
1395
+ this.log(`Skipping AI check - ${activeProcesses.length} active child process(es): ${names}`);
1396
+ this.logAction('detection', `Skipped AI check: child processes running (${names})`);
1397
+ this.startNoOutputTimer();
1398
+ this.startPreFilterTimer();
1399
+ return;
1400
+ }
1378
1401
  // If AI check is disabled or errored out, fall back to direct idle confirmation
1379
1402
  if (!this.config.aiIdleCheckEnabled || this.aiChecker.status === 'disabled') {
1380
1403
  this.log(`AI check unavailable (${this.aiChecker.status}), confirming idle directly via: ${reason}`);
@@ -1459,7 +1482,7 @@ export class RespawnController extends EventEmitter {
1459
1482
  if (this._state === 'stopped')
1460
1483
  return; // Guard against stopped state
1461
1484
  if (this._state === 'ai_checking') {
1462
- const errorMsg = err instanceof Error ? err.message : String(err);
1485
+ const errorMsg = getErrorMessage(err);
1463
1486
  this.logAction('ai-check', `Failed: ${errorMsg.substring(0, 50)}`);
1464
1487
  this.emit('aiCheckFailed', errorMsg);
1465
1488
  this.setState('watching');
@@ -1637,7 +1660,7 @@ export class RespawnController extends EventEmitter {
1637
1660
  }
1638
1661
  })
1639
1662
  .catch((err) => {
1640
- const errorMsg = err instanceof Error ? err.message : String(err);
1663
+ const errorMsg = getErrorMessage(err);
1641
1664
  this.emit('planCheckFailed', errorMsg);
1642
1665
  this.logAction('plan-check', `Failed: ${errorMsg.substring(0, 50)}`);
1643
1666
  });
@@ -1899,6 +1922,17 @@ export class RespawnController extends EventEmitter {
1899
1922
  this.startPreFilterTimer();
1900
1923
  return;
1901
1924
  }
1925
+ // Safety check: if child processes are running (bash tools, test suites, builds, etc.)
1926
+ const activeProcesses = this.session.getActiveChildProcesses();
1927
+ if (activeProcesses.length > 0) {
1928
+ const names = activeProcesses.map((p) => p.command).join(', ');
1929
+ this.log(`Idle confirmation rejected - ${activeProcesses.length} active child process(es): ${names}`);
1930
+ this.logAction('detection', `Rejected: child processes running (${names})`);
1931
+ this.setState('watching');
1932
+ this.startNoOutputTimer();
1933
+ this.startPreFilterTimer();
1934
+ return;
1935
+ }
1902
1936
  this.log(`Idle confirmed via: ${reason}`);
1903
1937
  const status = this.getDetectionStatus();
1904
1938
  this.log(`Detection status: confidence=${status.confidenceLevel}%, ` +