Skip to content

fix(scheduler): diagnose execFile maxBuffer errors and bump limit to 10MB (#719)#720

Merged
Kewton merged 1 commit into
developfrom
feature/719-worktree
May 23, 2026
Merged

fix(scheduler): diagnose execFile maxBuffer errors and bump limit to 10MB (#719)#720
Kewton merged 1 commit into
developfrom
feature/719-worktree

Conversation

@Kewton
Copy link
Copy Markdown
Owner

@Kewton Kewton commented May 22, 2026

Summary

  • src/lib/session/claude-executor.tsexecFile エラーハンドリングを改修し、ERR_CHILD_PROCESS_STDIO_MAXBUFFER を含む全エラーで Error / Code / Signal / Reason を必ず result 先頭に残すよう変更
  • exitCodetypeof errCode === 'number' で型ガードし、文字列コードは null を保存(従来は parseInt で NaN → null、原因不明化)
  • maxBuffer 超過は時間切れではなく「出力過多」なので timeout 扱いにせず failed のままにする(Codex 助言反映)
  • MAX_OUTPUT_SIZE を 1MB → 10MB に暫定緩和(根本対策の spawn + rolling buffer 化は別Issueへ)

Closes #719

変更内容

修正前の問題

Node v24.1.0 で execFile の stdout が maxBuffer=1MB を超えると、コールバックには {code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER', signal: undefined, killed: undefined} が渡される。現行コードでは:

  • isTimeout = error.killed (undefined) || code === 'ETIMEDOUT' → false → status='failed'
  • parseInt('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', 10) || null → null → exit_code=NULL
  • stdout || stderr || error.message の優先順で、巨大 stdout に押されて error.message が消失

その結果、DB の execution_logs には原因不明な `status=failed / exit_code=NULL / result=切り詰めた stdout` だけが残り、運用で原因特定不能だった。

修正後

```ts
const errCode = (error as NodeJS.ErrnoException).code;
const isMaxBuffer = errCode === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
const isTimeout = error.killed || errCode === 'ETIMEDOUT';

const errorSummary = [
`Error: ${error.message}`,
`Code: ${errCode ?? 'unknown'}`,
`Signal: ${error.signal ?? 'none'}`,
isMaxBuffer ? 'Reason: stdout exceeded execFile maxBuffer (output_limit)' : null,
].filter(Boolean).join('\n');

const rawOutput = stripAnsi([
errorSummary,
stdout ? `\n--- stdout ---\n${stdout}` : '',
stderr ? `\n--- stderr ---\n${stderr}` : '',
].join('\n'));

resolve({
output: truncateOutput(rawOutput),
exitCode: typeof errCode === 'number' ? errCode : null,
status: isTimeout ? 'timeout' : 'failed',
error: error.message,
});
```

Test plan

  • npm run lint → ✅ No ESLint warnings or errors
  • npx tsc --noEmit → ✅ クリーン
  • npx vitest run tests/unit/lib/claude-executor.test.ts → ✅ 37 tests passed
  • 新規テスト追加:maxBuffer 超過時に result 先頭へ Code: ERR_CHILD_PROCESS_STDIO_MAXBUFFERReason: stdout exceeded execFile maxBuffer が記録される
  • 新規テスト追加:外部 SIGTERM kill 時に Signal: SIGTERM が記録される
  • 新規テスト追加:exit code 1 終了時に Code: 1 が記録され exit_code=1 で保存される
  • 既存テスト:timeout / 正常終了は従来通り

影響範囲

  • src/lib/session/claude-executor.ts(38 行追加 / 5 行変更)
  • tests/unit/lib/claude-executor.test.ts(テストケース追加)
  • スキーマ変更なし(既存 execution_logs.result に診断情報を入れる)
  • UI 表示は非破壊(Schedules タブの実行ログ表示の冒頭に診断行が出る)

残課題(別Issueへ)

  • P3: execFilespawn + tail-rolling buffer 化(10MB ですら超える可能性のあるケースの根本対策)
  • 運用:codex プロンプト指針(広範囲 rg 抑制 等)

🤖 Generated with Claude Code

…0MB (#719)

Node v24 で execFile の `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` が
`failed/exit_code=null` のまま原因不明で終了していた問題を修正。

- error.code が文字列のとき parseInt で NaN → null になっていたのを
  `typeof errCode === 'number'` での厳密判定に変更(型安全)。
- `stdout || stderr || error.message` の優先順位で巨大 stdout が
  error.message を呑み込んでいたため、必ず先頭に Error/Code/Signal/Reason
  サマリを置き、続けて --- stdout --- / --- stderr --- セクションで
  両方を保存するように変更。
- ERR_CHILD_PROCESS_STDIO_MAXBUFFER の場合は専用 Reason 行を付加し、
  timeout ではなく failed として扱う(時間切れと出力過多は意味が異なる)。
- MAX_OUTPUT_SIZE を 1MB → 10MB に暫定緩和(spawn + rolling buffer 化は
  別 Issue で根本対策)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Kewton Kewton added the bug Something isn't working label May 22, 2026
@Kewton Kewton merged commit 2c27f3c into develop May 23, 2026
5 checks passed
Kewton added a commit that referenced this pull request May 27, 2026
* feat: 履歴(History)表示件数を50〜250件で選択可能にする (#701) (#702)

* feat(history): allow selecting history display limit (50-250)

Issue #701

- Add HistoryDisplayLimit selector (50/100/150/200/250) to HistoryPane header
- Persist selection in localStorage (commandmate:historyDisplayLimit)
- Raise messages API upper bound from 100 to MAX_MESSAGES_LIMIT (250)
- Centralize options/MAX/DEFAULT in src/config/history-display-config.ts
- Propagate selection through WorktreeDetailRefactored (PC + Mobile)
- worktreeApi.getMessages: optional limit argument
- useInfiniteMessages: default pageSize references DEFAULT_MESSAGES_LIMIT
- Tests: boundary (1/250/251/0/abc), DB getMessages limit=250,
  useInfiniteMessages hasMore at pageSize=250, config Single Source of Truth

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(history-display-config): derive MAX_MESSAGES_LIMIT from union type

- Replace brittle `as 250` literal cast with `HistoryDisplayLimit` type
  derivation so extending HISTORY_DISPLAY_LIMIT_OPTIONS no longer
  requires touching MAX_MESSAGES_LIMIT.
- Tighten comments and adjust wording for clarity.

Issue #701

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(claude-md): add history-display-config.ts to module reference (#701)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(detection): handle Claude v2.1.142 skill approval prompt summary line (#704) (#705)

Claude Code v2.1.142 renders trailing summary lines like "… +1 pending"
below the real options in skill approval prompts ("Use skill ...?").
NORMAL_OPTION_PATTERN was matching these as option N with label="pending",
which poisoned isValidPrecedingOption() and caused every real option
above (1./2./3.) to be rejected — collectedOptions ended up with length
< 2, returning no_prompt. As a result Yes/No UI never appeared and
Auto-Yes did not fire.

Defense-in-depth fix:
- S1: SUMMARY_LINE_PATTERN early-continue in Pass 2 loop (tight
  anchored pattern, no free `(.+)` capture, keyword whitelist
  pending|more) so legitimate labels mentioning these words are
  preserved.
- S2: CLAUDE_PROMPT_FOOTER_PATTERN trims effectiveEnd to the
  "Esc to cancel · Tab to amend" footer, putting the summary line
  outside the scan window entirely. Fallback-safe: no footer found =>
  effectiveEnd untouched, so Codex/Gemini/OpenCode/Copilot detection
  paths are unaffected.
- S3: 3-layer regression tests across prompt-detector, status-detector,
  and auto-yes-resolver covering the real Claude v2.1.142 fixture plus
  FP-suppression for option labels containing "pending"/"more".

Quality gates: lint 0/0, tsc 0, unit 6486 passed / 7 skipped, build OK.

* fix(files): preserve scroll position during tree refetch (#706) (#707)

- FileTreeView: limit full-screen loading/error to initial mount (rootItems empty)
- FileTreeView: add non-destructive refetch indicator (aria-live=polite) and error banner with retry button
- FileTreeView: extract reloadTreeWithExpandedDirs via useCallback + mountedRef guard
- WorktreeDetailRefactored: skip first-poll false-positive refresh by treating null prevTreeHashRef as baseline-only
- tests: add 5 new tests for refetch indicator / non-destructive error / retry path

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(sync): parallelize scanMultipleRepositories with Promise.allSettled (#711) (#712)

Each `git worktree list` spawns an independent child process, so running
them sequentially via `for ... await` made the sync endpoint scale linearly
in the number of repositories. Switch to `Promise.allSettled(...map(...))`
so all repository scans run concurrently while preserving the prior "one
failure does not abort the rest" semantics. Logs continue to include
`repoPath` so the (now unordered) output can still be correlated.

The unit test mock for `child_process` had to be switched from auto-mock to
a factory mock: vitest's auto-mock preserves `util.promisify.custom` from
the real `exec`, which made `promisify(exec)` bypass `mockImplementation`
entirely. The factory returns a fresh `vi.fn()` without that symbol so
callback-style mocking works.

Closes #711

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(sidebar): adaptive polling interval for active/idle transitions (#710) (#713)

useWorktreesCache のポーリング間隔(active=5s/idle=30s)が startPolling() 呼び出し時点で固定され、worktrees の active/idle 状態が遷移しても interval が更新されない問題を修正。

- currentIntervalRef: useRef<number | null> を追加し活動中の interval を追跡
- startPolling/stopPolling/hasActiveSession を useCallback でフック直下にリフトアップ
- worktrees-change useEffect で desired interval を再計算し差分時のみ startPolling を再実行
- document.hidden 中・初期化前(ref === null)はガードして no-op
- 既存の visibilitychange ハンドラの責務は維持

Closes #710

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(sidebar): consolidate useWorktreesCache via Provider Context (#709) (#714)

Sessions ページが useWorktreesCache() を直接呼び出していたため、
WorktreesCacheProvider と合わせて /api/worktrees ポーリングが2系統
並行で動いていた問題を解消。

- WorktreesCacheProvider に WorktreesCacheContext を新設し
  useWorktreesCacheContext() フックを export
- src/app/sessions/page.tsx を Context 経由参照に変更
- useWorktreesCache フック自体は不変 (テスト/単体利用は維持)
- 新規テスト tests/unit/components/providers/WorktreesCacheProvider.test.tsx

Closes #709

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(db): add composite index including role column on chat_messages (#708) (#715)

chat_messages の相関サブクエリ (role='assistant'/'user' AND archived=0 の
MAX(timestamp)/ORDER BY timestamp DESC LIMIT 1) が、既存
idx_messages_archived(worktree_id, archived, timestamp DESC) では role 列を
含まないため行スキャンに陥り、件数増加に伴い線形劣化していた。

Migration v32 で複合インデックス
  idx_messages_worktree_role_archived_time(worktree_id, role, archived, timestamp DESC)
を新設し、既存 idx_messages_archived を DROP。
他の role なしクエリ (getMessages, getLastMessage, deleteAllMessages) は
idx_messages_worktree_time(worktree_id, timestamp DESC) で従来同等に動作する。

- src/lib/db/migrations/v32-add-messages-role-composite-index.ts: 新規
- src/lib/db/migrations/index.ts / runner.ts: v32 登録、CURRENT_SCHEMA_VERSION = 32
- src/lib/db/init-db.ts: 初期化パスを新インデックスへ置換
- tests/unit/lib/db-migrations.test.ts: v32 describe ブロック追加、
  v22 旧インデックスアサーションを rollback to 31 経由に修正
- tests/unit/lib/db/chat-db-explain-plan.test.ts: EXPLAIN QUERY PLAN
  で対象4クエリが新インデックスを使うことを検証

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: Worktree詳細 HistoryPaneにメッセージテキスト検索機能を追加 (#716) (#717)

* feat(history): add text search to HistoryPane (#716)

Adds a per-pane text search bar to Worktree HistoryPane with namespace-isolated
CSS Custom Highlight API rendering so it can coexist with the TerminalPane
search (Issue #47).

Key design choices (3-stage reviewed):
- Preserve OCP: existing applyTerminalHighlights / clearTerminalHighlights
  signatures unchanged. New applyHistoryHighlights / clearHistoryHighlights
  share the internal engine via HighlightNamespace.
- ConversationPairCard receives no new props; only data-message-id is added so
  memo is preserved. useConversationHistory is untouched.
- HistoryPane manages autoExpandedIds internally to force-expand truncated
  assistant messages that contain hits, then applies highlights with a strict
  useLayoutEffect order (save scroll → restore scroll (skipped during search)
  → autoExpandedIds → applyHistoryHighlights) so textContent stays aligned
  with message.content.
- Debounce (300ms), min-query length (2), and max-matches (500) are shared
  via newly exported SEARCH_DEBOUNCE_MS / SEARCH_MIN_QUERY_LENGTH /
  TERMINAL_SEARCH_MAX_MATCHES from useTerminalSearch.

Tests: 47 new cases across useHistorySearch, HistorySearchBar, terminal-
highlight, and HistoryPane. Full unit suite: 6558 passed / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(history-search): DRY debounce timer + unify selector escape (#716)

Small post-implementation cleanups identified during the refactoring phase.
No behavior changes; all 6558 unit tests still pass.

useHistorySearch.ts:
- Extract clearDebounceTimer() helper (DRY: clearTimeout/null pattern was
  duplicated across scheduleSearch / closeSearch / onCompositionStart /
  unmount cleanup).

HistoryPane.tsx:
- Replace custom escapeAttrValue() with native CSS.escape() via a small
  findMessageElement() helper. Removes a homemade escape function that
  duplicated standards-track functionality and unifies the two
  inconsistent selector-building sites (one used CSS.escape, the other
  the custom helper).
- Capture the current match element during the highlight loop so the
  scrollIntoView path no longer issues a second querySelector for the
  same node.

Constraints preserved:
- terminal-highlight.ts public API untouched (OCP).
- ConversationPairCard props unchanged (memo preserved).
- Effect declaration order (1)->(2)->(3)->(4) and HISTORY_SEARCH_NAMESPACE
  isolation unchanged (design policy v1.2 core).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(history): update CLAUDE.md and implementation-history for #716

Add HistoryPane text search feature entry and document new modules:
useHistorySearch hook, HistorySearchBar component, terminal-highlight
namespace separation (HISTORY_SEARCH_NAMESPACE), and data-message-id
attribution in ConversationPairCard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(executor): diagnose execFile maxBuffer errors and bump limit to 10MB (#719) (#720)

Node v24 で execFile の `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` が
`failed/exit_code=null` のまま原因不明で終了していた問題を修正。

- error.code が文字列のとき parseInt で NaN → null になっていたのを
  `typeof errCode === 'number'` での厳密判定に変更(型安全)。
- `stdout || stderr || error.message` の優先順位で巨大 stdout が
  error.message を呑み込んでいたため、必ず先頭に Error/Code/Signal/Reason
  サマリを置き、続けて --- stdout --- / --- stderr --- セクションで
  両方を保存するように変更。
- ERR_CHILD_PROCESS_STDIO_MAXBUFFER の場合は専用 Reason 行を付加し、
  timeout ではなく failed として扱う(時間切れと出力過多は意味が異なる)。
- MAX_OUTPUT_SIZE を 1MB → 10MB に暫定緩和(spawn + rolling buffer 化は
  別 Issue で根本対策)。

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant