claude-spotter 1.1.5 → 1.1.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6
4
+
5
+ **Bell の isolated `CLAUDE_CONFIG_DIR` が Spotter haiku の auth を破壊する bug を修正**。bellbot 等で CLAUDE_CONFIG_DIR を分離する運用 (user scope MCP を流入させない隔離設計、credentials 非共有) のとき、hook → daemon → haiku の spawn 連鎖で Bell の isolated config が継承され、Spotter haiku が credentials 不在の config を読みに行き exit 1。その後同じ session-id が claude CLI 側で "already in use" と判定されて失敗が固定化し、user_input hook が非 0 exit し続けてベル本体のプロンプト処理が破綻していた。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: 純粋関数 `sanitizeHaikuEnv(baseEnv)` を named export として新設、`createHaikuCaller` の spawn env 構築時に `CLAUDE_CONFIG_DIR` を strip。haiku はデフォルト `~/.claude/` で起動する
10
+ - **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `runHaikuJudgment` で `E_HAIKU_TIMEOUT` / `E_INTERNAL` (auth / spawn / exit != 0) 発生時も throw 前に `callHaiku.reset()` で session を rotate。haiku-caller は成功時のみ `isFirstCall=false` を倒す設計なので、失敗が続くと同じ UUID を `--session-id` で再送して claude CLI 側の "already in use" に化ける経路を塞ぐ
11
+ - **編集 [test/haiku-caller.test.mjs](test/haiku-caller.test.mjs)**: `sanitizeHaikuEnv` の回帰テスト 2 件 (strip 動作 / absent 時 no-op + 原本非破壊)
12
+ - **編集 [test/daemon.test.mjs](test/daemon.test.mjs)**: `runHaikuJudgment` が E_INTERNAL / E_HAIKU_TIMEOUT で reset() を呼ぶ回帰テスト 2 件
13
+
14
+ ### 背景
15
+
16
+ #### 指摘経路
17
+
18
+ 2026-04-20 外部指摘で、bellbot 相当の運用 (`CLAUDE_CONFIG_DIR=~/.bellbot-claude-config` + `--dangerously-skip-permissions`) で Discord → Bell ベース agent が無反応になる障害が報告された。daemon log に以下の連鎖:
19
+
20
+ ```
21
+ [13:28:57] handler error on user_input: E_INTERNAL: haiku exited with code 1:
22
+ [13:29:11] handler error on user_input: E_INTERNAL: haiku exited with code 1: Error: Session ID 4625feeb-... is already in use.
23
+ [13:30:25] handler error on user_input: E_INTERNAL: haiku exited with code 1: Error: Session ID 4625feeb-... is already in use.
24
+ ```
25
+
26
+ コード監査で (1) Bell の CLAUDE_CONFIG_DIR が spawn 継承で剥がされずに daemon / haiku まで到達していること、(2) [haiku-caller.mjs:311-313](src/daemon/haiku-caller.mjs#L311-L313) が `isFirstCall` を成功時のみ false に倒す設計のため失敗した uuid が固定化すること、の二点を実コードで確認。
27
+
28
+ #### strip 範囲をなぜ haiku-caller だけに限定したか
29
+
30
+ Spotter の catalog 調査 (`claude mcp list`, `claude mcp get`) は Bell が実際に見える MCP セットを反映する必要があるため、Bell の CLAUDE_CONFIG_DIR を尊重する。Haiku の推論エンジンだけが Spotter 側の credentials を必要とする。したがって strip は `claude -p` 呼出し (credentials-requiring call) のみで行う。`investigate-mcp.mjs` の `execClaude` や stdio MCP spawn は env 無加工継承を維持。
31
+
32
+ #### session rotate を追加した理由
33
+
34
+ CLAUDE_CONFIG_DIR の strip だけでは、将来の auth / network / quota / CLI crash 等の異なる失敗源でも同じ "session-id 固定化 → 失敗連鎖" 構造を再生産する。haiku-caller が isFirstCall を成功時のみ倒す設計である以上、daemon 側で HaikuError を掴んだ時点で必ず reset を呼ぶのが構造的な解。これは §0 silent fallback 禁止とは別軸の「unexpected でも内部 state は clean に保つ」防御。
35
+
36
+ ### 設計判断
37
+
38
+ - **`SPOTTER_CLAUDE_CONFIG_DIR` 等のユーザー向け override は未導入**: 指摘の proposed direction で副次案として挙がっていたが、現状 strip で十分解決。ユーザーが Spotter の config を別 dir にしたい具体的ユースケース (例: Spotter 専用 API key の隔離) が出たら再検討
39
+ - **他 env (`ANTHROPIC_API_KEY` 等) は strip しない**: ユーザーが明示的に設定した API key は両方に適用されるべき。strip 対象は「Bell セッション固有で、Haiku にとって誤動作源になる」 `CLAUDE_CONFIG_DIR` 一点のみ
40
+ - **reset 箇所は `runHaikuJudgment` の catch 一点に集約**: handler 毎 (handleUserInput / handleTurnEnd) に書かない。haiku 呼出しは必ず runHaikuJudgment を通る設計なのでそこで閉じる
41
+
3
42
  ## 1.1.5
4
43
 
5
44
  **Windows で refresh 毎に cmd.exe console window が flash + 入力フォーカスを奪う UX 回帰を修正**。`listMcpServers` / `getStdioConfig` が `execClaude` 経由で spawn する `cmd.exe /c claude mcp list/get` に `windowsHide: true` が付いておらず、SessionStart 毎の bg refresh と install 時 seed で毎回黒いウィンドウが一瞬表示されキーボード入力が奪われていた。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -136,9 +136,27 @@ export async function startDaemon({
136
136
 
137
137
  // v0.5.0: shared Haiku-invocation + parse helper. On E_HAIKU_SCHEMA (role collapse),
138
138
  // rotates the Haiku session-id and silent-passes the turn (reason: role_collapse_reset).
139
- // Other Haiku errors (timeout, spawn failure) still propagate — §14 unexpected → throw.
139
+ //
140
+ // v1.1.6: E_HAIKU_TIMEOUT / E_INTERNAL (spawn failure, auth failure, exit != 0) は
141
+ // §14 unexpected として throw するが、throw の前に必ず callHaiku.reset() で session を
142
+ // 回転する。haiku-caller は成功時のみ isFirstCall=false を倒す設計なので、失敗が続くと
143
+ // 同じ UUID を `--session-id` 再送 → claude CLI 側で "Session ID ... is already in use"
144
+ // に化けて失敗連鎖が固定化していた。reset で次 turn が fresh id + preamble 再送から
145
+ // やり直せるようにする (真因 — 例えば CLAUDE_CONFIG_DIR 誤継承による auth 失敗 — が
146
+ // 解消されたら即回復可能な状態を保つ)。silent fallback 新規導入ではない、§0 「想定済み
147
+ // 異常 = 記録 + 正常リターン」とは別軸の「unexpected でも内部 state は clean に保つ」
148
+ // 防御。
140
149
  const runHaikuJudgment = async (stage, prompt) => {
141
- const { raw, meta } = await callHaikuTracked(prompt);
150
+ let raw, meta;
151
+ try {
152
+ ({ raw, meta } = await callHaikuTracked(prompt));
153
+ } catch (err) {
154
+ if (err instanceof HaikuError && typeof callHaiku.reset === 'function') {
155
+ logFn(`${stage}: haiku invocation failed (${err.code}), rotating session before rethrow: ${err.message}`);
156
+ callHaiku.reset();
157
+ }
158
+ throw err;
159
+ }
142
160
  let parsed;
143
161
  try {
144
162
  parsed = parseHaikuResponse(raw);
@@ -219,6 +219,18 @@ function truncate(s, n = 300) {
219
219
  return s.slice(0, n) + '...';
220
220
  }
221
221
 
222
+ // Bell の isolated CLAUDE_CONFIG_DIR (例: bellbot プロファイル) が hook → daemon → haiku の
223
+ // spawn 連鎖で継承されると、Spotter haiku が credentials 不在の config を読みに行き exit 1。
224
+ // その後 session-id が「already in use」で stuck して user_input hook が非 0 exit し続ける。
225
+ // v1.1.6: spawn env 構築時に CLAUDE_CONFIG_DIR を剥がし、デフォルト ~/.claude/ で Haiku を
226
+ // 起動する。監査対象の Claude CLI 側 (Bell の env で走る `claude mcp list` 等) は意図通り
227
+ // Bell の config を参照するため、strip はこの一点 (credentials が必要な claude -p 呼出し)
228
+ // のみで行う。
229
+ export function sanitizeHaikuEnv(baseEnv) {
230
+ const { CLAUDE_CONFIG_DIR: _strip, ...rest } = baseEnv;
231
+ return rest;
232
+ }
233
+
222
234
  // On Windows, the `claude` entry is typically a .cmd shim which Node's spawn cannot locate
223
235
  // without going through the shell. We use cmd.exe /c explicitly rather than spawn({ shell:
224
236
  // true }) because the latter triggers DEP0190 on Node 24+.
@@ -264,7 +276,7 @@ export function createHaikuCaller({ preamble, timeoutMs, claudeBin = 'claude', m
264
276
  });
265
277
  const child = spawn(cmd, cmdArgs, {
266
278
  cwd: WORKDIR,
267
- env: { ...env, SPOTTER_PARENT_PID: String(process.pid) },
279
+ env: { ...sanitizeHaikuEnv(env), SPOTTER_PARENT_PID: String(process.pid) },
268
280
  stdio: ['pipe', 'pipe', 'pipe'],
269
281
  windowsHide: true,
270
282
  });