claude-spotter 1.9.3 → 1.9.4
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 +11 -0
- package/package.json +1 -1
- package/src/cli/codex-hook-cmd.mjs +4 -0
- package/src/cli/grok-hook-cmd.mjs +2 -2
- package/src/core/hook-event-log.mjs +21 -1
- package/src/platform/spawn.mjs +43 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。
|
|
4
4
|
現行runtime契約は[`docs/00_overview.md`](https://github.com/kitepon/Spotter/blob/main/docs/00_overview.md)から辿る。
|
|
5
5
|
|
|
6
|
+
## 1.9.4 — 2026-10-09
|
|
7
|
+
|
|
8
|
+
- Windowsで、監査の制限時間と同時にCodexが自分で終わった時、`taskkill`がrootを見つけられず(exit 128)、
|
|
9
|
+
Spotterが「process treeの終了を確認できない」(`E_CODEX_CLI_TERMINATION`)として扱っていた。この失敗は
|
|
10
|
+
汎用の失敗として`SPOTTER.AUDITOR.UNAVAILABLE`へ即時登録される。child自身のclose(終了と全stdio pipeのEOF)を
|
|
11
|
+
確認できた時だけ終了済みとし、監査はtimeoutのまま扱う。closeを確認できない時と、128以外の`taskkill`失敗は
|
|
12
|
+
従来どおり`E_CODEX_CLI_TERMINATION`にする。
|
|
13
|
+
- CodexとGrokの失敗hook eventへ、失敗した段の内部code(`internalCode`)と、あればその原因のcode・終了status
|
|
14
|
+
(`causeCode`、`causeExitCode`)を残す。これまでは表示用にまとめた`code`だけが残り、1回の失敗の種類を
|
|
15
|
+
logから読み戻せなかった。記録するのは固定の識別子と終了statusだけで、hookの出力とstderrは変えていない。
|
|
16
|
+
|
|
6
17
|
## 1.9.3 — 2026-10-07
|
|
7
18
|
|
|
8
19
|
- 監査backendへ届かない・応じてもらえない失敗(network、timeout、認証、利用上限、5xx)を、1回ごとに
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../hooks/parent-output-projector.mjs';
|
|
29
29
|
import {
|
|
30
30
|
appendHookEvent,
|
|
31
|
+
failureEventFields,
|
|
31
32
|
hookEventsPath,
|
|
32
33
|
summarizeHookEvents,
|
|
33
34
|
} from '../core/hook-event-log.mjs';
|
|
@@ -228,6 +229,7 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
228
229
|
status: 'error',
|
|
229
230
|
backend: err?.backend ?? null,
|
|
230
231
|
code: failure.code,
|
|
232
|
+
...failureEventFields(err),
|
|
231
233
|
...compactCodexModelSelectionForEvent(err?.diagnostics?.modelSelection),
|
|
232
234
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
233
235
|
durationMs: Date.now() - startedAt,
|
|
@@ -339,6 +341,7 @@ export async function runCodexStopHook({
|
|
|
339
341
|
hook: 'Stop',
|
|
340
342
|
status: 'error',
|
|
341
343
|
code: failure.code,
|
|
344
|
+
...failureEventFields(err),
|
|
342
345
|
reason: 'tool_usage_observation',
|
|
343
346
|
usedToolCount: 0,
|
|
344
347
|
durationMs: Date.now() - startedAt,
|
|
@@ -395,6 +398,7 @@ export async function runCodexStopHook({
|
|
|
395
398
|
status: 'error',
|
|
396
399
|
backend: err?.backend ?? null,
|
|
397
400
|
code: failure.code,
|
|
401
|
+
...failureEventFields(err),
|
|
398
402
|
...compactCodexModelSelectionForEvent(err?.diagnostics?.modelSelection),
|
|
399
403
|
usedToolCount: usedTools.length,
|
|
400
404
|
...toolUsageEvent,
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { createAuditorBackend } from '../core/auditor-backend.mjs';
|
|
8
8
|
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
9
|
-
import { appendHookEventSafe } from '../core/hook-event-log.mjs';
|
|
9
|
+
import { appendHookEventSafe, failureEventFields } from '../core/hook-event-log.mjs';
|
|
10
10
|
import { projectBackendFailure, projectToolIds } from '../hooks/parent-output-projector.mjs';
|
|
11
11
|
import { findSpotterMarker, isChildCall, readStdinJson, requireString } from '../hooks/lib.mjs';
|
|
12
12
|
import { readLocal, refresh } from '../tool-db/refresh.mjs';
|
|
@@ -154,7 +154,7 @@ export async function runGrokHook({
|
|
|
154
154
|
const failure = projectBackendFailure(error?.code);
|
|
155
155
|
writeError(failure.stderr);
|
|
156
156
|
await record(projectRoot, expected, {
|
|
157
|
-
status: 'degraded', code: failure.code, durationMs: Date.now() - startedAt,
|
|
157
|
+
status: 'degraded', code: failure.code, ...failureEventFields(error), durationMs: Date.now() - startedAt,
|
|
158
158
|
}, recordFn);
|
|
159
159
|
if (prompt !== null) await recordGrokEvaluation({ createEvaluationStoreFn, projectRoot, input, prompt, status: 'error', writeError });
|
|
160
160
|
return;
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
// backendDurationMs?: number | null,
|
|
25
25
|
// usedToolCount?: number,
|
|
26
26
|
// legacyPendingDiagnostic?: string | null,
|
|
27
|
-
// toolName?: string | null
|
|
27
|
+
// toolName?: string | null,
|
|
28
|
+
// internalCode?: string | null, // failing step's own code; `code` is the projected one
|
|
29
|
+
// causeCode?: string,
|
|
30
|
+
// causeExitCode?: number
|
|
28
31
|
// }
|
|
29
32
|
|
|
30
33
|
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
@@ -34,6 +37,23 @@ const HOOK_EVENTS_FILE = 'hook-events.jsonl';
|
|
|
34
37
|
export const HOOK_EVENT_SCHEMA = 'spotter.hook_event.v1';
|
|
35
38
|
export const HOOK_EVENTS_SUMMARY_SCHEMA = 'spotter.hook_events_summary.v1';
|
|
36
39
|
|
|
40
|
+
const EVENT_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
41
|
+
|
|
42
|
+
function eventCode(value) {
|
|
43
|
+
return typeof value === 'string' && EVENT_CODE_PATTERN.test(value) ? value : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// `code` holds the projected, user-facing code, which folds many failures into one generic value.
|
|
47
|
+
// These fields keep the failing step's own code so a single failure can be read back from the log.
|
|
48
|
+
// Only fixed identifiers and an exit status are taken; messages and provider output never are.
|
|
49
|
+
export function failureEventFields(error) {
|
|
50
|
+
const fields = { internalCode: eventCode(error?.code) };
|
|
51
|
+
const causeCode = eventCode(error?.cause?.code);
|
|
52
|
+
if (causeCode !== null) fields.causeCode = causeCode;
|
|
53
|
+
if (Number.isInteger(error?.cause?.exitCode)) fields.causeExitCode = error.cause.exitCode;
|
|
54
|
+
return fields;
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
export function hookEventsPath(projectRoot) {
|
|
38
58
|
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
39
59
|
throw new TypeError('hookEventsPath: projectRoot must be a non-empty string');
|
package/src/platform/spawn.mjs
CHANGED
|
@@ -134,12 +134,41 @@ export function npmShimEntryPath(source, shimDir) {
|
|
|
134
134
|
return win32.join(shimDir, ...segments);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// taskkill は root の pid が存在しない時に 128 で終わる。この時 taskkill は何も終了していない。
|
|
138
|
+
const TASKKILL_PROCESS_NOT_FOUND = 128;
|
|
139
|
+
|
|
140
|
+
// 'close' は process の終了と全 stdio pipe の EOF を意味する。root より長生きした子孫が
|
|
141
|
+
// pipe を持っている間は stream が閉じないので、ここは false になる。
|
|
142
|
+
function childHasClosed(child) {
|
|
143
|
+
if (!Number.isInteger(child.exitCode) && typeof child.signalCode !== 'string') return false;
|
|
144
|
+
return [child.stdin, child.stdout, child.stderr].every((stream) => !stream || stream.destroyed === true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function waitForChildClose(child, graceMs) {
|
|
148
|
+
if (childHasClosed(child)) return Promise.resolve(true);
|
|
149
|
+
if (typeof child.once !== 'function' || typeof child.off !== 'function') return Promise.resolve(false);
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const onClose = () => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve(true);
|
|
154
|
+
};
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
child.off('close', onClose);
|
|
157
|
+
resolve(childHasClosed(child));
|
|
158
|
+
}, graceMs);
|
|
159
|
+
child.once('close', onClose);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
// cmd.exe shim のみ kill すると孫の CLI が残り得るため、Windows は process tree を
|
|
138
164
|
// taskkill で終了する。taskkill 自体の起動・終了に失敗した時だけ direct kill へ戻す。
|
|
165
|
+
// taskkill が root を見つけられなかった時は、child 自身の close を確認できた場合だけ終了済みとする。
|
|
166
|
+
// timeout と同時に自分で終わった process を「終了未確認」にしないためで、close が無ければ従来どおり失敗する。
|
|
139
167
|
export function terminateProcessTree(child, {
|
|
140
168
|
platform = process.platform,
|
|
141
169
|
spawnFn = spawn,
|
|
142
170
|
timeoutMs = 5_000,
|
|
171
|
+
closeGraceMs = 500,
|
|
143
172
|
} = {}) {
|
|
144
173
|
if (!child || typeof child.kill !== 'function') return Promise.resolve();
|
|
145
174
|
if (platform !== 'win32' || !Number.isSafeInteger(child.pid) || child.pid <= 0) {
|
|
@@ -184,9 +213,20 @@ export function terminateProcessTree(child, {
|
|
|
184
213
|
finish();
|
|
185
214
|
return;
|
|
186
215
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
const fail = () => {
|
|
217
|
+
const error = new Error(`taskkill exited with code ${code}`);
|
|
218
|
+
error.code = 'E_PROCESS_TREE_TERMINATION';
|
|
219
|
+
error.exitCode = code;
|
|
220
|
+
finish(error);
|
|
221
|
+
};
|
|
222
|
+
if (code !== TASKKILL_PROCESS_NOT_FOUND) {
|
|
223
|
+
fail();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
waitForChildClose(child, closeGraceMs).then((closed) => {
|
|
227
|
+
if (closed) finish();
|
|
228
|
+
else fail();
|
|
229
|
+
});
|
|
190
230
|
});
|
|
191
231
|
} catch (cause) {
|
|
192
232
|
const error = new Error(`taskkill spawn failed: ${cause.message}`);
|