Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/3416.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- ### Fixed -->
- **fix(agent-health): git の読み取りが 1 秒のタイムアウトで打ち切られたときの `git:command-failed` を ERROR から warn に下げる** (#3416): 本番ログの 127 件はすべて負荷時の `status --porcelain` 等のタイムアウト(stderr 空)だった。git が非 0 で終わった本当の失敗は ERROR のまま。ログに worktree 名(basename)と `timedOut` / `timeoutMs` を足し、次の計測で呼び出し元を数えられるようにした
2 changes: 1 addition & 1 deletion docs/module-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ const myState = getOrInitGlobal('__myState', () => new Map<string, MyState>());
| `src/lib/env-manager/env-file-service.ts` | Env Manager のファイル I/O(**サーバ専用**、`fs/promises` を import、Issue #1968)。`resolveEnvFilePath()` が 3 層のパス検証(allowlist → `isPathSafe` [SF-002] → `resolveAndValidateRealPath` [SEC-394])を順に適用。`listEnvFiles()`(ワークツリー直下のみ・非再帰。root が解決できないと `realpathSync` が投げてパス検証が fail-closed になり `[]` を返す=望ましい方向)、`readEnvFile()`(未存在はエラーでなく `exists:false`)、`writeEnvFile()`(新規作成は `mode 0o600`、検証 error があれば書かずに `INVALID_CONTENT`)、`collectKeySuggestions()`。**値も `fs` のエラーメッセージもログに出さない**(記録するのはファイル名と errno のみ) |
| `src/lib/env-manager/env-api-client.ts` | Env Manager のブラウザ側 fetch(`fs` を巻き込まないため service とは別モジュール、Issue #1968)。`fetchEnvSnapshot()`(`cache: 'no-store'`)/ `saveEnvFile()` / `EnvApiError`(code と、値を含まない issues を保持) |
| `src/lib/env-manager/types.ts` | Env Manager の API 型(Issue #1968)。`EnvFileSummary` / `EnvKeySuggestion` / `EnvFileDetail` / `EnvManagerResponse` / `EnvSaveResponse` / `EnvErrorResponse`。route.ts は HTTP メソッドしか export できない(`scripts/check-route-exports.mjs`、Issue #1946)ため lib 側に置く |
| `src/lib/git/git-utils.ts` | Git情報取得(getGitStatus関数、execFile使用、1秒タイムアウト)。Git情報取得・コミット履歴/diff取得(Issue #447)、getCommitsByDateRange/collectRepositoryCommitLogs追加(Issue #627)、getAheadBehind追加(`git rev-list --left-right --count @{upstream}...HEAD`・厳密パース・left=behind/right=ahead・全失敗null・非throw、`getGitStatus`は不変)(Issue #779)。Issue #780で git write 操作群追加:`parsePorcelainStatus`(`git status --porcelain` を `{staged,unstaged,untracked}` に分類、X=index/Y=worktree、`??`→untracked、`U*`/`AA`/`DD`→unmerged は unstaged バケット、rename は new path)、`getStagedStatus`、`stageFiles`(`git add --`)、`unstageFiles`(`git restore --staged --`)、`gitCommit`(`git commit -m <msg> [--amend] --`、`GitNothingToCommitError`正規化)、`getWorkingTreeDiff`(mode=staged→`git diff --cached`/unstaged→`git diff`/untracked→`git diff --no-index /dev/null`、exit-1+stdout 回収)。排他制御=module-level `writeChains: Map<worktreePath,Promise>` 直列化+`.git/index.lock` existsSync チェック→`GitIndexLockedError`。`handleGitApiError` を 409(`GitIndexLockedError`)/400(`GitNothingToCommitError`) に拡張。write系は `execGitCommandTyped`+`GIT_WRITE_TIMEOUT_MS`。**`getGitStatus` と 1s `execGitCommand` はバイト不変(#779 invariant)**。Issue #781で branch 操作追加:read `listBranches(worktreePath, include)`(`git branch [-r]`+`symbolic-ref refs/remotes/origin/HEAD`(default)+`git worktree list --porcelain`(checkedOutWorktreePath 逆引き)+`git for-each-ref`(upstream/aheadBehind)を1s `execGitCommand` で並列実行・各 best-effort 縮退・**非throw**、`parseWorktreePorcelain`/`parseForEachRefTracking` 新パーサ)。write `checkoutBranch`(precondition=別worktree占有→`GitBranchCheckedOutElsewhereError`(force不可)/dirty→`GitDirtyError`、remote→`git switch -c <local> --track origin/<b>` で detached 回避 S3-008、createIfMissing→`switch -c`、force→`checkout -f`)/`createBranch`(`git branch <name> [from] --`)/`deleteBranch`(current→`GitCurrentBranchError`/default→`GitDefaultBranchError`、`-d` not-merged→`GitBranchNotMergedError`)。全 write は `runSerializedWrite`+`execGitCommandTyped(GIT_WRITE_TIMEOUT_MS)`・branch 引数 `--` 終端。新 error class 6種+`handleGitApiError` を `{error, reason}` で 404 branch_not_found/409 not_merged/current_branch/default_branch/dirty/checked_out_elsewhere(+worktreePath) にマップ。**`getGitStatus`/1s read path は #781 でもバイト不変(invariant 回帰テストあり)**。Issue #782で stash + reset/revert(Danger Zone)追加:read `parseStashList`/`getStashList`(`git stash list --format='%gd%x09%s%x09%cI%x09%H'`・1s `execGitCommand` 非throw・失敗時 `[]` 縮退)。write `stashPush`(`git stash push [--include-untracked] [-m] --`、`No local changes`→`GitNothingToStashError`正規化)/`stashPop`/`stashApply`/`stashDrop`/`gitReset`(`git reset --<mode> <target> --`)/`gitRevert`(`git revert [--no-commit] <hash> --`)を `runSerializedWrite`+`GIT_WRITE_TIMEOUT_MS`+`--` 終端で実装。pop/apply/revert の conflict は `execGitConflictAware`(`execFileAsync` 直呼びで `err.stdout` から `CONFLICT ... in <path>` 回収し **200 `{conflict,conflictFiles}`**、pop は `stashRetained:true`、timeout→`GitTimeoutError`/not-repo→`GitNotRepoError` を手動再現・index.lock は `runSerializedWrite` 経路で担保 S3-002)。hard reset は `isDefaultBranchForReset`(`symbolic-ref refs/remotes/origin/HEAD`・**未解決時 main/master フォールバック**=deleteBranch とは意図的非対称 S3-010)で default branch を判定し専用 `GitResetDefaultBranchError`(409、delete 文言とは別文言 S3-001(b))。新 error class `GitNothingToStashError`(400)/`GitResetDefaultBranchError`(409)+`handleGitApiError` を `GitTimeoutError` 手前に additive 拡張(**既存 #780/#781 reason body バイト不変・回帰テストあり**)。危険操作(hard reset/stash drop/revert)は `logger.warn` 構造化ログ。**`getGitStatus`/1s read path は #782 でもバイト不変**。Issue #783で network 操作(push/pull/fetch)追加:`getDefaultBranch`(`symbolic-ref refs/remotes/origin/HEAD --short` を **3値**=解決名/`DEFAULT_BRANCH_UNRESOLVED`/非origin→null で返す DR1-002)+`resolveDefaultBranchName`(UNRESOLVED/null を null に畳む)共有ヘルパーを新設し、`isDefaultBranchForReset`/`deleteBranch` を**バイト不変**に集約(DR1-001、listBranches は別関心事で非集約)。`execGitNetworkAware`(`execFileAsync` 直呼び・preserve regex 不変)+`classifyNetworkStderr`(network stderr→6 typed error 単一ソース・**引数なし構築で raw stderr/credential URL 非露出** DR4-003)。`gitFetch`(非直列化=remote-tracking ref のみ・§6.1)/`gitPull`(`runSerializedWrite`+`execGitConflictAware(timeout, classifyNetwork=true)`=conflict 200・generic 失敗は raw `err.message` 非ログ DR4-002)/`gitPush`(**サーバー構築の明示 refspec `${branch}:refs/heads/${branch}`** で destination を決定論化し `push.default`/upstream bypass を封鎖 DR4-004、force 時に `resolveDefaultBranchName` と比較→`GitProtectedBranchError`(409)・unresolved=未保護=reset と非対称、`--force-with-lease` 優先、force push は `logger.warn` 構造化ログ)。`execGitConflictAware` に `timeout`/`classifyNetwork` 引数を additive 追加(#782 caller はバイト不変 DR2-007)。新 error class 6種(`GitAuthFailedError`401/`GitNonFastForwardError`409/`GitNoUpstreamError`400/`GitProtectedBranchError`409/`GitForceWithLeaseStaleError`409/`GitNetworkError`**502**)+`handleGitApiError` を `GitTimeoutError` 手前に additive 拡張(既存 byte-identical it 不変・DR3-003)。**`getGitStatus`/1s read path/preserve regex は #783 でもバイト不変**。Issue #1515で ahead/behind の鮮度対策:`getAheadBehind` は `{aheadBehind, reason}` を返す形に変更(`execGitCommandCapture`=非throwのまま stderr を保持する read helper を新設し、stderr を `no_upstream`/`upstream_gone`/`detached`/`error` に分類。timeout は常に `error`。**raw stderr は返さずログにも出さない**・分類 enum のみ)。`getLastFetchAt`=`git rev-parse --git-path FETCH_HEAD --git-common-dir` の両 FETCH_HEAD mtime の**新しい方**を epoch ms で返す(linked worktree はこの worktree での fetch と repo root での fetch の双方を拾う・未fetchは null・非throw)。`execGitNetworkAware` は `GIT_TERMINAL_PROMPT=0` 付きで実行(認証情報なし HTTPS remote で対話プロンプト待ちにならず即 `GitAuthFailedError`) |
| `src/lib/git/git-utils.ts` | Git情報取得(getGitStatus関数、execFile使用、1秒タイムアウト)。Git情報取得・コミット履歴/diff取得(Issue #447)、getCommitsByDateRange/collectRepositoryCommitLogs追加(Issue #627)、getAheadBehind追加(`git rev-list --left-right --count @{upstream}...HEAD`・厳密パース・left=behind/right=ahead・全失敗null・非throw、`getGitStatus`は不変)(Issue #779)。Issue #780で git write 操作群追加:`parsePorcelainStatus`(`git status --porcelain` を `{staged,unstaged,untracked}` に分類、X=index/Y=worktree、`??`→untracked、`U*`/`AA`/`DD`→unmerged は unstaged バケット、rename は new path)、`getStagedStatus`、`stageFiles`(`git add --`)、`unstageFiles`(`git restore --staged --`)、`gitCommit`(`git commit -m <msg> [--amend] --`、`GitNothingToCommitError`正規化)、`getWorkingTreeDiff`(mode=staged→`git diff --cached`/unstaged→`git diff`/untracked→`git diff --no-index /dev/null`、exit-1+stdout 回収)。排他制御=module-level `writeChains: Map<worktreePath,Promise>` 直列化+`.git/index.lock` existsSync チェック→`GitIndexLockedError`。`handleGitApiError` を 409(`GitIndexLockedError`)/400(`GitNothingToCommitError`) に拡張。write系は `execGitCommandTyped`+`GIT_WRITE_TIMEOUT_MS`。**`getGitStatus` と 1s `execGitCommand` はバイト不変(#779 invariant)**。Issue #781で branch 操作追加:read `listBranches(worktreePath, include)`(`git branch [-r]`+`symbolic-ref refs/remotes/origin/HEAD`(default)+`git worktree list --porcelain`(checkedOutWorktreePath 逆引き)+`git for-each-ref`(upstream/aheadBehind)を1s `execGitCommand` で並列実行・各 best-effort 縮退・**非throw**、`parseWorktreePorcelain`/`parseForEachRefTracking` 新パーサ)。write `checkoutBranch`(precondition=別worktree占有→`GitBranchCheckedOutElsewhereError`(force不可)/dirty→`GitDirtyError`、remote→`git switch -c <local> --track origin/<b>` で detached 回避 S3-008、createIfMissing→`switch -c`、force→`checkout -f`)/`createBranch`(`git branch <name> [from] --`)/`deleteBranch`(current→`GitCurrentBranchError`/default→`GitDefaultBranchError`、`-d` not-merged→`GitBranchNotMergedError`)。全 write は `runSerializedWrite`+`execGitCommandTyped(GIT_WRITE_TIMEOUT_MS)`・branch 引数 `--` 終端。新 error class 6種+`handleGitApiError` を `{error, reason}` で 404 branch_not_found/409 not_merged/current_branch/default_branch/dirty/checked_out_elsewhere(+worktreePath) にマップ。**`getGitStatus`/1s read path は #781 でもバイト不変(invariant 回帰テストあり)**。Issue #782で stash + reset/revert(Danger Zone)追加:read `parseStashList`/`getStashList`(`git stash list --format='%gd%x09%s%x09%cI%x09%H'`・1s `execGitCommand` 非throw・失敗時 `[]` 縮退)。write `stashPush`(`git stash push [--include-untracked] [-m] --`、`No local changes`→`GitNothingToStashError`正規化)/`stashPop`/`stashApply`/`stashDrop`/`gitReset`(`git reset --<mode> <target> --`)/`gitRevert`(`git revert [--no-commit] <hash> --`)を `runSerializedWrite`+`GIT_WRITE_TIMEOUT_MS`+`--` 終端で実装。pop/apply/revert の conflict は `execGitConflictAware`(`execFileAsync` 直呼びで `err.stdout` から `CONFLICT ... in <path>` 回収し **200 `{conflict,conflictFiles}`**、pop は `stashRetained:true`、timeout→`GitTimeoutError`/not-repo→`GitNotRepoError` を手動再現・index.lock は `runSerializedWrite` 経路で担保 S3-002)。hard reset は `isDefaultBranchForReset`(`symbolic-ref refs/remotes/origin/HEAD`・**未解決時 main/master フォールバック**=deleteBranch とは意図的非対称 S3-010)で default branch を判定し専用 `GitResetDefaultBranchError`(409、delete 文言とは別文言 S3-001(b))。新 error class `GitNothingToStashError`(400)/`GitResetDefaultBranchError`(409)+`handleGitApiError` を `GitTimeoutError` 手前に additive 拡張(**既存 #780/#781 reason body バイト不変・回帰テストあり**)。危険操作(hard reset/stash drop/revert)は `logger.warn` 構造化ログ。**`getGitStatus`/1s read path は #782 でもバイト不変**。Issue #783で network 操作(push/pull/fetch)追加:`getDefaultBranch`(`symbolic-ref refs/remotes/origin/HEAD --short` を **3値**=解決名/`DEFAULT_BRANCH_UNRESOLVED`/非origin→null で返す DR1-002)+`resolveDefaultBranchName`(UNRESOLVED/null を null に畳む)共有ヘルパーを新設し、`isDefaultBranchForReset`/`deleteBranch` を**バイト不変**に集約(DR1-001、listBranches は別関心事で非集約)。`execGitNetworkAware`(`execFileAsync` 直呼び・preserve regex 不変)+`classifyNetworkStderr`(network stderr→6 typed error 単一ソース・**引数なし構築で raw stderr/credential URL 非露出** DR4-003)。`gitFetch`(非直列化=remote-tracking ref のみ・§6.1)/`gitPull`(`runSerializedWrite`+`execGitConflictAware(timeout, classifyNetwork=true)`=conflict 200・generic 失敗は raw `err.message` 非ログ DR4-002)/`gitPush`(**サーバー構築の明示 refspec `${branch}:refs/heads/${branch}`** で destination を決定論化し `push.default`/upstream bypass を封鎖 DR4-004、force 時に `resolveDefaultBranchName` と比較→`GitProtectedBranchError`(409)・unresolved=未保護=reset と非対称、`--force-with-lease` 優先、force push は `logger.warn` 構造化ログ)。`execGitConflictAware` に `timeout`/`classifyNetwork` 引数を additive 追加(#782 caller はバイト不変 DR2-007)。新 error class 6種(`GitAuthFailedError`401/`GitNonFastForwardError`409/`GitNoUpstreamError`400/`GitProtectedBranchError`409/`GitForceWithLeaseStaleError`409/`GitNetworkError`**502**)+`handleGitApiError` を `GitTimeoutError` 手前に additive 拡張(既存 byte-identical it 不変・DR3-003)。**`getGitStatus`/1s read path/preserve regex は #783 でもバイト不変**。Issue #1515で ahead/behind の鮮度対策:`getAheadBehind` は `{aheadBehind, reason}` を返す形に変更(`execGitCommandCapture`=非throwのまま stderr を保持する read helper を新設し、stderr を `no_upstream`/`upstream_gone`/`detached`/`error` に分類。timeout は常に `error`。**raw stderr は返さずログにも出さない**・分類 enum のみ)。`getLastFetchAt`=`git rev-parse --git-path FETCH_HEAD --git-common-dir` の両 FETCH_HEAD mtime の**新しい方**を epoch ms で返す(linked worktree はこの worktree での fetch と repo root での fetch の双方を拾う・未fetchは null・非throw)。`execGitNetworkAware` は `GIT_TERMINAL_PROMPT=0` 付きで実行(認証情報なし HTTPS remote で対話プロンプト待ちにならず即 `GitAuthFailedError`) **Issue #3416**: `execGitCommand`(git-exec.ts)は 1 秒タイムアウトで打ち切られた読み取りを `git:command-failed` の warn(`timedOut`/`timeoutMs` つき)で記録し、git が非 0 で終わった失敗だけ ERROR のまま。両方ともログに worktree の basename を載せる。 |
| `src/lib/utils.ts` | 汎用ユーティリティ関数(debounce、truncateString、escapeHtml等)。汎用ユーティリティ(withTimeout追加: Issue #627) |
| `src/lib/global-state.ts` | `getOrInitGlobal(key, init)`: `globalThis[key]` に値があればそれを返し、無ければ `init` を 1 回だけ実行して置く(Issue #3231)。`src/` の `globalThis.__*` の初期化はこの呼び出しにそろえた(冒頭の規約を参照)。import を持たない |
| `src/config/editable-extensions.ts` | 編集可能ファイル拡張子設定。編集可能拡張子定義・バリデーション(EDITABLE_EXTENSIONS, EXTENSION_VALIDATORS, isEditableExtension, validateContent)。.yaml/.yml 追加・YAML危険タグバリデーション(Issue #646)。TEXT_MAX_SIZE_BYTES を 2MB に引き上げ・PUT/GET 共通定数化(Issue #723) **Issue #2506: `.txt` を追加**(`EDITABLE_EXTENSIONS` は 5 → 6 要素、`EXTENSION_VALIDATORS` に `maxFileSize: TEXT_MAX_SIZE_BYTES` のエントリ)。`additionalValidation` は**付けない**。YAML の危険タグ検査(#646)が要るのは `.yaml` がどこかで **parse される**からで、プレーンテキストは parse されないため同じバイト列を拒む理由が無い。NULL バイト検出と制御文字警告は `validateContent()` の共通処理なので `.txt` にもそのまま効く。配列への追加は**末尾**(先頭挿入ではない): `NewFileDialog` がこの配列を順にドロップダウンへ描画し `selectedExt` の初期値を `'.md'` にしているため、末尾に足すことで既存の選択肢の並びと既定値が変わらない。1 要素の追加が同時に 3 つの振る舞いを変える点に注意 —— PUT の書き込み許可(`isEditableFile()`)、PC ファイルパネルのエディタ分岐(`FilePanelContent`)、そして **GET の 2MB 上限適用**(`evaluateEditableSizeLimit()` が `isEditableExtension()` で分岐するため)。3 番目は #2505 が「上限超過 = 読めない」を「上限超過 = 保存できない」に変えた後だから安全であって、#2505 以前に同じ追加をしていたら 2MB 超の `.txt` が 413 で開けなくなっていた。`EDITABLE_EXTENSIONS` / `isEditableExtension()` のシグネチャは #2507(モバイル側)が前提にするので変えないこと |
Expand Down
Loading
Loading