codeloop-mcp-server 0.1.55 → 0.1.57

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.
package/dist/index.js CHANGED
@@ -174,6 +174,35 @@ startUpdateCheck();
174
174
  * issues we've already written to disk continue to gate ready_for_review.
175
175
  */
176
176
  const modalPersistenceTracker = new Map();
177
+ /**
178
+ * 0.1.57 H2 — Throttle for the post-interact modal probe.
179
+ *
180
+ * 0.1.55 only ran the probe after SUCCESSFUL clicky/escape actions, so a
181
+ * file dialog that blocked the app (subsequent clicks fail, or the agent
182
+ * switches to wait / capture_screenshot / win_ui_automate) was never
183
+ * re-detected and the persistence counter never climbed — the modal sat
184
+ * open with zero cycle issues recorded (E2E #12). We now probe on the
185
+ * broader modal-related action set REGARDLESS of success, and use this map
186
+ * to keep the PowerShell cost bounded: clicky/escape always probe; other
187
+ * modal-related actions probe at most once per throttle window per app.
188
+ */
189
+ const modalProbeLastAt = new Map();
190
+ const MODAL_PROBE_THROTTLE_MS = 1500;
191
+ /**
192
+ * 0.1.57 H3 — auto-close stuck external file dialogs.
193
+ *
194
+ * Default ON. The user can opt out per-machine with
195
+ * CODELOOP_AUTO_CLOSE_STUCK_MODALS=0 (or "false"/"off") if a workflow
196
+ * legitimately keeps a file picker open across many interactions.
197
+ */
198
+ function autoCloseStuckModalsEnabled() {
199
+ const v = (process.env.CODELOOP_AUTO_CLOSE_STUCK_MODALS ?? "").trim().toLowerCase();
200
+ if (v === "0" || v === "false" || v === "off" || v === "no")
201
+ return false;
202
+ return true;
203
+ }
204
+ /** Consecutive same-dialog detections that mark a file dialog as genuinely stuck. */
205
+ const MODAL_AUTOCLOSE_THRESHOLD = 3;
177
206
  const server = new McpServer({
178
207
  name: "codeloop",
179
208
  version: "0.1.14",
@@ -1924,6 +1953,59 @@ After stopping, call codeloop_interaction_replay with the run_id to extract fram
1924
1953
  The response includes log_path if app logs were captured during the recording session.`, {
1925
1954
  recording_id: z.string().describe("The recording_id returned by codeloop_start_recording"),
1926
1955
  }, async (params) => {
1956
+ // 0.1.57 H4 — stuck-modal sweep. If a file dialog is still open when the
1957
+ // recording stops (E2E #12: the external folder picker that "kept opening
1958
+ // all the time"), close it automatically so it does not linger on the
1959
+ // user's desktop after the cycle. Best-effort and Windows-only; never
1960
+ // blocks the stop. Runs BEFORE finalising so the close is visible in the
1961
+ // final frames.
1962
+ let sweepNote;
1963
+ if (process.platform === "win32" && autoCloseStuckModalsEnabled()) {
1964
+ try {
1965
+ const { detectModal } = await import("./runners/modal_detector.js");
1966
+ const { closeModalWithStrategies } = await import("./runners/modal_close_strategies.js");
1967
+ const vrMod = await import("./runners/video_recorder.js");
1968
+ const appNameForModal = vrMod.getActiveRecordingAppName() || undefined;
1969
+ const detection = await detectModal({
1970
+ target_type: "desktop",
1971
+ app_name: appNameForModal,
1972
+ cwd: projectDir,
1973
+ });
1974
+ if (detection.is_modal_present && detection.modal_kind === "file_dialog") {
1975
+ const closeResult = await closeModalWithStrategies({
1976
+ initial_detection: detection,
1977
+ app_name: appNameForModal,
1978
+ cwd: projectDir,
1979
+ });
1980
+ const { recordCycleIssue } = await import("./evidence/cycle_issues.js");
1981
+ if (closeResult.closed) {
1982
+ const usedStrategy = closeResult.strategies_tried.find((s) => s.success)?.strategy ?? "ladder";
1983
+ sweepNote = `CodeLoop auto-closed a lingering file dialog (${detection.modal_description ?? "(unnamed)"}) at stop via ${usedStrategy}.`;
1984
+ await recordCycleIssue(projectDir, {
1985
+ kind: "modal_close_failed",
1986
+ modal_kind: detection.modal_kind ?? "file_dialog",
1987
+ modal_description: `${detection.modal_description ?? "(unnamed)"} (auto-closed by CodeLoop H4 stop sweep via ${usedStrategy})`,
1988
+ strategies_tried: closeResult.strategies_tried.filter((s) => s.success).map((s) => s.strategy),
1989
+ hwnd: detection.hwnd,
1990
+ auto_resolved: true,
1991
+ });
1992
+ }
1993
+ else {
1994
+ sweepNote = `A file dialog (${detection.modal_description ?? "(unnamed)"}) was still open at stop and the close ladder failed — call codeloop_kill_modal_window with hwnd ${detection.hwnd ?? "(see logs)"}.`;
1995
+ await recordCycleIssue(projectDir, {
1996
+ kind: "modal_close_failed",
1997
+ modal_kind: detection.modal_kind ?? "file_dialog",
1998
+ modal_description: detection.modal_description,
1999
+ strategies_tried: closeResult.strategies_tried.map((s) => s.strategy),
2000
+ hwnd: detection.hwnd ?? closeResult.hwnd,
2001
+ });
2002
+ }
2003
+ }
2004
+ }
2005
+ catch {
2006
+ /* best-effort sweep — never block the stop */
2007
+ }
2008
+ }
1927
2009
  const authResult = await withAuth(async () => {
1928
2010
  const { stopBackgroundRecording } = await import("./runners/video_recorder.js");
1929
2011
  return stopBackgroundRecording(params.recording_id);
@@ -1945,6 +2027,7 @@ The response includes log_path if app logs were captured during the recording se
1945
2027
  " 1. codeloop_interaction_replay — extract frames + app logs from the just-saved video. This populates the data the replay/journey gates score against.",
1946
2028
  " 2. codeloop_gate_check — confirm confidence ≥ 94%. If continue_fixing, fix the failing gate's next_step and re-record / re-capture.",
1947
2029
  "Do NOT skip step 1 — without replay frames the interaction_replay_evidence gate fails even when the video exists. Do NOT pause to ask the user 'should I run replay now?' — yes, always.",
2030
+ ...(sweepNote ? ["", `[CodeLoop H4] ${sweepNote}`] : []),
1948
2031
  ].join("\n");
1949
2032
  return {
1950
2033
  content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) + nextStepDirective }]),
@@ -3538,15 +3621,40 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3538
3621
  const isClosingIntent = /\b(close|dismiss|cancel|escape|exit)\b/.test(closingIntent);
3539
3622
  const isClickyAction = action === "click" || action === "double_click" || action === "right_click";
3540
3623
  const isEscapeKeystroke = action === "keystroke" && (params.key ?? "").toLowerCase() === "escape";
3541
- const shouldVerifyClickEffect = success && tt === "desktop" && (isClickyAction || isEscapeKeystroke);
3542
- if (shouldVerifyClickEffect) {
3624
+ // 0.1.57 H2 probe on the broader modal-related action set REGARDLESS
3625
+ // of success. A blocked / failed click against a stuck file dialog is
3626
+ // itself the signal we need (E2E #12: subsequent clicks failed, so the
3627
+ // 0.1.55 `success &&` gate never re-ran the probe and the picker stayed
3628
+ // open with zero cycle issues). Clicky/escape always probe; the wider
3629
+ // set (type/hotkey/win_ui_automate/sequence/wait) probes at most once
3630
+ // per throttle window so the PowerShell cost stays bounded.
3631
+ const trackerKey = (params.app_name || vr.getActiveRecordingAppName() || "<default>").toLowerCase();
3632
+ const clicky = isClickyAction || isEscapeKeystroke;
3633
+ const widerModalActions = new Set([
3634
+ "type",
3635
+ "type_and_submit",
3636
+ "type_and_tab",
3637
+ "keystroke",
3638
+ "hotkey",
3639
+ "win_ui_automate",
3640
+ "sequence",
3641
+ "wait",
3642
+ "hover",
3643
+ ]);
3644
+ const nowMs = Date.now();
3645
+ const lastProbeMs = modalProbeLastAt.get(trackerKey) ?? 0;
3646
+ const throttleOk = clicky || (widerModalActions.has(action) && nowMs - lastProbeMs > MODAL_PROBE_THROTTLE_MS);
3647
+ const shouldCheckModal = tt === "desktop" && throttleOk;
3648
+ if (shouldCheckModal) {
3543
3649
  try {
3650
+ modalProbeLastAt.set(trackerKey, nowMs);
3544
3651
  await new Promise((resolve) => setTimeout(resolve, 500));
3545
3652
  const { detectModal } = await import("./runners/modal_detector.js");
3546
- const trackerKey = (params.app_name || vr.getActiveRecordingAppName() || "<default>").toLowerCase();
3653
+ const { closeModalWithStrategies } = await import("./runners/modal_close_strategies.js");
3654
+ const appNameForModal = params.app_name || vr.getActiveRecordingAppName() || undefined;
3547
3655
  const detection = await detectModal({
3548
3656
  target_type: "desktop",
3549
- app_name: params.app_name || vr.getActiveRecordingAppName() || undefined,
3657
+ app_name: appNameForModal,
3550
3658
  cwd,
3551
3659
  });
3552
3660
  const { recordCycleIssue } = await import("./evidence/cycle_issues.js");
@@ -3605,11 +3713,79 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3605
3713
  modal_kind: detection.modal_kind,
3606
3714
  });
3607
3715
  }
3608
- // 0.1.55 F3at 3+ consecutive persistences, treat the modal
3609
- // as actively stuck and write a modal_close_failed entry so
3610
- // the cycle_issues_acknowledged gate fails even when the
3611
- // agent never called codeloop_handle_modal.
3612
- if (consecutive >= 3) {
3716
+ // 0.1.57 H3AUTOMATIC close of stuck external file dialogs.
3717
+ //
3718
+ // E2E #12: the agent rationalised a recurring OpenFolderDialog as
3719
+ // a "test artifact" and NEVER called codeloop_handle_modal, so the
3720
+ // picker was left open and kept reopening. CodeLoop must close it
3721
+ // itself rather than only directing the agent.
3722
+ //
3723
+ // Trigger conditions (all required) keep this safe against legit
3724
+ // picker usage:
3725
+ // - it's a file_dialog (external OS picker — the exact class the
3726
+ // user reports; confirm/alert kinds may need a real decision
3727
+ // and are intentionally left to the agent),
3728
+ // - it has persisted across >= MODAL_AUTOCLOSE_THRESHOLD (3)
3729
+ // consecutive detections of the SAME dialog. A legitimate
3730
+ // open→confirm picker flow clears within 1-2 interactions and
3731
+ // resets the tracker, so reaching 3 means the agent is stuck.
3732
+ const isStuckFileDialog = detection.modal_kind === "file_dialog" &&
3733
+ consecutive >= MODAL_AUTOCLOSE_THRESHOLD;
3734
+ let autoClosed = false;
3735
+ if (isStuckFileDialog && autoCloseStuckModalsEnabled()) {
3736
+ try {
3737
+ const closeResult = await closeModalWithStrategies({
3738
+ initial_detection: detection,
3739
+ app_name: appNameForModal,
3740
+ cwd,
3741
+ });
3742
+ autoClosed = closeResult.closed;
3743
+ if (autoClosed) {
3744
+ modalPersistenceTracker.delete(trackerKey);
3745
+ const usedStrategy = closeResult.strategies_tried.find((s) => s.success)?.strategy ?? "ladder";
3746
+ detail = `${detail} | CodeLoop auto-closed stuck file dialog (${desc}) via ${usedStrategy}`;
3747
+ clickEffectVerification = {
3748
+ ...(clickEffectVerification ?? { intent: closingIntent, modal_still_present: false }),
3749
+ modal_still_present: false,
3750
+ modal_description: detection.modal_description,
3751
+ modal_kind: detection.modal_kind,
3752
+ consecutive_persistences: consecutive,
3753
+ };
3754
+ // Surface the auto-handling so the dev report shows it was
3755
+ // caught and resolved (not silently swallowed).
3756
+ await recordCycleIssue(cwd, {
3757
+ kind: "modal_close_failed",
3758
+ modal_kind: detection.modal_kind ?? "file_dialog",
3759
+ modal_description: `${desc} (auto-closed by CodeLoop H3 after ${consecutive} consecutive detections via ${usedStrategy})`,
3760
+ strategies_tried: closeResult.strategies_tried
3761
+ .filter((s) => s.success)
3762
+ .map((s) => s.strategy),
3763
+ hwnd: detection.hwnd,
3764
+ auto_resolved: true,
3765
+ });
3766
+ }
3767
+ else {
3768
+ // Ladder exhausted — record the unresolved failure and
3769
+ // escalate the directive to kill-window.
3770
+ await recordCycleIssue(cwd, {
3771
+ kind: "modal_close_failed",
3772
+ modal_kind: detection.modal_kind ?? "file_dialog",
3773
+ modal_description: desc,
3774
+ strategies_tried: closeResult.strategies_tried.map((s) => s.strategy),
3775
+ hwnd: detection.hwnd ?? closeResult.hwnd,
3776
+ });
3777
+ }
3778
+ }
3779
+ catch {
3780
+ /* best-effort auto-close; fall through to the F3/F4 path */
3781
+ }
3782
+ }
3783
+ // 0.1.55 F3 — at 3+ consecutive persistences, treat the modal as
3784
+ // actively stuck and write a modal_close_failed entry so the
3785
+ // cycle_issues_acknowledged gate fails even when the agent never
3786
+ // called codeloop_handle_modal. Skipped when H3 already recorded
3787
+ // the (auto-resolved or unresolved) outcome above.
3788
+ if (consecutive >= 3 && !isStuckFileDialog) {
3613
3789
  await recordCycleIssue(cwd, {
3614
3790
  kind: "modal_close_failed",
3615
3791
  modal_kind: detection.modal_kind ?? "custom",
@@ -3620,14 +3796,20 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3620
3796
  hwnd: detection.hwnd,
3621
3797
  });
3622
3798
  }
3623
- // 0.1.55 F4 — HARD directive for the post-interact postscript.
3624
- modalPersistenceDirective =
3625
- `\n\n[CodeLoop F4] HARD: A ${detection.modal_kind ?? "modal"} dialog (${desc}) is STILL present after this interaction ` +
3626
- `(${consecutive} consecutive interactions have not cleared it). ` +
3627
- `Stop sending raw clicks / Escape keystrokes against it and call codeloop_handle_modal with decision: "cancel" or "dismiss" — ` +
3628
- `the multi-strategy ladder (Escape → Alt+F4 → UIA Invoke "Close" → EndDialog) handles file dialogs the keystroke path cannot. ` +
3629
- `If codeloop_handle_modal returns escalation: "kill_window_required", call codeloop_kill_modal_window with the returned hwnd. ` +
3630
- `Continuing to ignore this modal will fail the cycle_issues_acknowledged gate and block ready_for_review.`;
3799
+ // 0.1.55 F4 / 0.1.57 H3 — HARD directive for the post-interact
3800
+ // postscript. Suppressed when CodeLoop already auto-closed the
3801
+ // dialog; otherwise escalated to kill-window when an auto-close
3802
+ // attempt was made and failed.
3803
+ if (!autoClosed) {
3804
+ const killHint = isStuckFileDialog
3805
+ ? `CodeLoop already attempted the multi-strategy close ladder and it FAILED — call codeloop_kill_modal_window with hwnd ${detection.hwnd ?? "(from codeloop_handle_modal)"} now. `
3806
+ : `Stop sending raw clicks / Escape keystrokes against it and call codeloop_handle_modal with decision: "cancel" or "dismiss" — the multi-strategy ladder (Escape Alt+F4 → UIA Invoke "Close" → EndDialog) handles file dialogs the keystroke path cannot. If codeloop_handle_modal returns escalation: "kill_window_required", call codeloop_kill_modal_window with the returned hwnd. `;
3807
+ modalPersistenceDirective =
3808
+ `\n\n[CodeLoop F4] HARD: A ${detection.modal_kind ?? "modal"} dialog (${desc}) is STILL present after this interaction ` +
3809
+ `(${consecutive} consecutive interactions have not cleared it). ` +
3810
+ killHint +
3811
+ `Continuing to ignore this modal will fail the cycle_issues_acknowledged gate and block ready_for_review.`;
3812
+ }
3631
3813
  }
3632
3814
  else {
3633
3815
  // Modal cleared — reset the tracker for this app so the next