agent-sanitizer 2.45.0 → 2.45.2
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.
|
@@ -198,6 +198,7 @@ export async function runJudgeCli(
|
|
|
198
198
|
payloadBytes,
|
|
199
199
|
tool,
|
|
200
200
|
cpuMs: timer.cpuMs(),
|
|
201
|
+
redactorMs: timer.redactorMs(),
|
|
201
202
|
}),
|
|
202
203
|
event,
|
|
203
204
|
),
|
|
@@ -214,6 +215,7 @@ export async function runJudgeCli(
|
|
|
214
215
|
payloadBytes,
|
|
215
216
|
tool,
|
|
216
217
|
cpuMs: timer.cpuMs(),
|
|
218
|
+
redactorMs: timer.redactorMs(),
|
|
217
219
|
});
|
|
218
220
|
onError(err, input);
|
|
219
221
|
}
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* before anyone traced it back here). A hook past the budget therefore says so
|
|
10
10
|
* IN BAND, in the model's context, where it can be relayed to the operator.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* THREE numbers, because wall-clock alone cannot say whose cost it is: a hook on
|
|
13
13
|
* a contended host waits far longer than it computes (a 1.1 KB payload and a
|
|
14
14
|
* 235 KB one both reported 7.2s on a loaded 2-vCPU box, against 0.3s of work).
|
|
15
|
-
* So the notice prints
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* So the notice prints, beside the clock, the CPU this process burned and the
|
|
16
|
+
* time it spent inside redactor round trips — the daemon is a separate,
|
|
17
|
+
* long-lived process whose CPU this one cannot see (see {@link processCpuMs}),
|
|
18
|
+
* so the call that waits for it is the only measurable stand-in. The notice
|
|
19
|
+
* GATES on none of them: a hook wedged on a dead redactor socket burns no CPU
|
|
20
|
+
* and is exactly the sanitizer's fault.
|
|
18
21
|
*
|
|
19
22
|
* ONE-TIME PROVISIONING is excluded (see {@link excludeProvisioning}): charging
|
|
20
23
|
* an install to the hook that merely waited it out would make the FIRST call of
|
|
@@ -90,13 +93,15 @@ export function formatBytes(bytes) {
|
|
|
90
93
|
* that made this specific latency report take a manual multi-step
|
|
91
94
|
* investigation to characterize (which tool call, how large a payload) before
|
|
92
95
|
* anyone could act on it.
|
|
93
|
-
* `cpuMs` is the run's own processor time
|
|
94
|
-
*
|
|
96
|
+
* `cpuMs` is the run's own processor time and `redactorMs` the wall-clock it
|
|
97
|
+
* spent inside redactor round trips (both from {@link startHookTimer}); absent
|
|
98
|
+
* when the caller has no way to measure them, which is what the shell port of
|
|
95
99
|
* this module reports.
|
|
96
100
|
* @typedef {{
|
|
97
101
|
* payloadBytes?: number | null,
|
|
98
102
|
* tool?: string | null,
|
|
99
103
|
* cpuMs?: number | null,
|
|
104
|
+
* redactorMs?: number | null,
|
|
100
105
|
* }} SlowHookContext
|
|
101
106
|
*/
|
|
102
107
|
|
|
@@ -138,6 +143,36 @@ function processCpuMs() {
|
|
|
138
143
|
let provisioningMs = 0;
|
|
139
144
|
let provisioningCpuMs = 0;
|
|
140
145
|
|
|
146
|
+
// Process-wide total of wall-clock spent inside redactor round trips. Wall-clock
|
|
147
|
+
// and not CPU because the daemon is a separate process: its work is invisible to
|
|
148
|
+
// processCpuMs, so how long this side waited for an answer is the only
|
|
149
|
+
// measurement available.
|
|
150
|
+
let redactorRoundTripMs = 0;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Run `work` — one redactor round trip — charging its duration to the redactor
|
|
154
|
+
* share, so a run that spent its second inside a redaction call is told apart
|
|
155
|
+
* from one that spent it anywhere else. Charged in a `finally`, since a round
|
|
156
|
+
* trip that THROWS (a stall that hit its deadline) is the one that spent the
|
|
157
|
+
* most.
|
|
158
|
+
*
|
|
159
|
+
* Unlike {@link excludeProvisioning} this only attributes; the time stays in the
|
|
160
|
+
* hook's wall-clock, because a slow redaction is a per-call cost the user waits
|
|
161
|
+
* for.
|
|
162
|
+
* @template T
|
|
163
|
+
* @param {() => Promise<T>} work
|
|
164
|
+
* @param {() => number} [now] injectable clock, for tests
|
|
165
|
+
* @returns {Promise<T>}
|
|
166
|
+
*/
|
|
167
|
+
export async function chargeRedactorRoundTrip(work, now = Date.now) {
|
|
168
|
+
const started = now();
|
|
169
|
+
try {
|
|
170
|
+
return await work();
|
|
171
|
+
} finally {
|
|
172
|
+
redactorRoundTripMs += Math.max(0, now() - started);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
/**
|
|
142
177
|
* Run `work`, charging its whole duration to provisioning so no timer running
|
|
143
178
|
* across it counts that time. Charged in a `finally`, so a provisioning step
|
|
@@ -177,24 +212,27 @@ export async function excludeProvisioning(
|
|
|
177
212
|
* so far MINUS any provisioning charged in the meantime, and may be called more
|
|
178
213
|
* than once.
|
|
179
214
|
*
|
|
180
|
-
* `wallMs` is what the user waited
|
|
181
|
-
* computed
|
|
182
|
-
*
|
|
215
|
+
* `wallMs` is what the user waited, `cpuMs` is what this process actually
|
|
216
|
+
* computed, and `redactorMs` is what it spent inside redactor round trips
|
|
217
|
+
* ({@link chargeRedactorRoundTrip}). All three are needed to say where a slow
|
|
218
|
+
* run's time went — see the module header for the report that read a contended
|
|
219
|
+
* host as a sanitizer bug.
|
|
183
220
|
*
|
|
184
|
-
*
|
|
185
|
-
* earlier run's cold start cannot pay down a later run's real cost
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* at 0.
|
|
221
|
+
* Every reader counts only what was charged since this timer started, so an
|
|
222
|
+
* earlier run's cold start cannot pay down a later run's real cost, and an
|
|
223
|
+
* earlier run's round trip cannot be blamed on this one. A provisioning window
|
|
224
|
+
* that straddles the timer's start would otherwise be able to subtract more than
|
|
225
|
+
* the timer has measured, so the results are floored at 0.
|
|
189
226
|
* @param {() => number} [now] injectable clock, for tests
|
|
190
227
|
* @param {() => number} [cpuNow] injectable CPU clock, for tests
|
|
191
|
-
* @returns {{ wallMs: () => number, cpuMs: () => number }}
|
|
228
|
+
* @returns {{ wallMs: () => number, cpuMs: () => number, redactorMs: () => number }}
|
|
192
229
|
*/
|
|
193
230
|
export function startHookTimer(now = Date.now, cpuNow = processCpuMs) {
|
|
194
231
|
const started = now();
|
|
195
232
|
const cpuStarted = cpuNow();
|
|
196
233
|
const provisionedBefore = provisioningMs;
|
|
197
234
|
const provisionedCpuBefore = provisioningCpuMs;
|
|
235
|
+
const redactorBefore = redactorRoundTripMs;
|
|
198
236
|
return {
|
|
199
237
|
wallMs: () =>
|
|
200
238
|
Math.max(0, now() - started - (provisioningMs - provisionedBefore)),
|
|
@@ -203,9 +241,47 @@ export function startHookTimer(now = Date.now, cpuNow = processCpuMs) {
|
|
|
203
241
|
0,
|
|
204
242
|
cpuNow() - cpuStarted - (provisioningCpuMs - provisionedCpuBefore),
|
|
205
243
|
),
|
|
244
|
+
redactorMs: () => Math.max(0, redactorRoundTripMs - redactorBefore),
|
|
206
245
|
};
|
|
207
246
|
}
|
|
208
247
|
|
|
248
|
+
/**
|
|
249
|
+
* The attribution sentence for a run whose CPU and redactor-round-trip shares
|
|
250
|
+
* are both known: the two numbers, then which of the three WINDOWS the time went
|
|
251
|
+
* into — this hook computing, the redactor call, or neither.
|
|
252
|
+
*
|
|
253
|
+
* A window, not a culprit. The round trip is wall-clock measured from this side,
|
|
254
|
+
* so it holds the daemon's scan AND whatever descheduling the host imposed on
|
|
255
|
+
* either end; claiming the daemon from it would re-commit, one bucket over, the
|
|
256
|
+
* overreach this whole split exists to retract. Separating those two needs
|
|
257
|
+
* telemetry from inside the daemon, and the wire protocol has no place to carry
|
|
258
|
+
* it — the response is `handle_request`'s object or a bare JSON `null`, which no
|
|
259
|
+
* sibling field can ride on. So the redactor verdict says WHERE and declines to
|
|
260
|
+
* say WHOSE, while the other two windows, which no host load can move time into,
|
|
261
|
+
* are named outright.
|
|
262
|
+
*
|
|
263
|
+
* The CPU and redactor shares overlap by the framing this side does
|
|
264
|
+
* mid-round-trip, so they do not sum to the elapsed time; a share that dominates
|
|
265
|
+
* despite the overlap is still the one to act on.
|
|
266
|
+
* @param {number} elapsedMs
|
|
267
|
+
* @param {number} cpuMs
|
|
268
|
+
* @param {number} redactorMs
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
function attributeWait(elapsedMs, cpuMs, redactorMs) {
|
|
272
|
+
const otherMs = Math.max(0, elapsedMs - cpuMs - redactorMs);
|
|
273
|
+
const verdict =
|
|
274
|
+
redactorMs >= cpuMs && redactorMs >= otherMs
|
|
275
|
+
? "The largest share was spent inside the redactor round trip — the daemon's scan, the host it shares, or both; this hook was not computing it."
|
|
276
|
+
: cpuMs >= otherMs
|
|
277
|
+
? "The largest share is this hook computing — a per-call cost the sanitizer owns, repeated by every affected call."
|
|
278
|
+
: "The largest share is neither the redactor nor this hook computing: it was blocked on a loaded machine or on something outside the sanitizer that it called.";
|
|
279
|
+
return (
|
|
280
|
+
`, of which ${formatSeconds(cpuMs)}s was this hook's own CPU and ` +
|
|
281
|
+
`${formatSeconds(redactorMs)}s was inside redactor round trips. ${verdict}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
209
285
|
/**
|
|
210
286
|
* The model-facing line for a hook that overran the budget, or null when it did
|
|
211
287
|
* not. Addressed to the model because the model is the only party that reliably
|
|
@@ -213,14 +289,16 @@ export function startHookTimer(now = Date.now, cpuNow = processCpuMs) {
|
|
|
213
289
|
* is asked to relay the numbers, since the operator is the one who can file it.
|
|
214
290
|
*
|
|
215
291
|
* With `context.cpuMs` in hand the line says which share of the wait was the
|
|
216
|
-
* sanitizer computing
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
292
|
+
* sanitizer computing, and with `context.redactorMs` too it names the window the
|
|
293
|
+
* time went into ({@link attributeWait}). Without them the line says that it cannot
|
|
294
|
+
* tell, rather than asserting an attribution nothing measured: a wall-clock
|
|
295
|
+
* overrun on a loaded host is the common case, and blaming it on the sanitizer
|
|
296
|
+
* sends the operator hunting a per-call cost that does not exist.
|
|
220
297
|
*
|
|
221
|
-
*
|
|
222
|
-
* that blocks on a dead socket inside a
|
|
223
|
-
* machine load, so naming either as the
|
|
298
|
+
* A CPU figure alone cannot pick a cause, so with only that the clause names
|
|
299
|
+
* candidates and commits to none. A hook that blocks on a dead socket inside a
|
|
300
|
+
* HOST extension spends no CPU and adds no machine load, so naming either as the
|
|
301
|
+
* cause would be a second wrong guess.
|
|
224
302
|
* @param {string} hookName
|
|
225
303
|
* @param {number} elapsedMs
|
|
226
304
|
* @param {number} [thresholdMs]
|
|
@@ -237,16 +315,24 @@ export function slowHookNotice(
|
|
|
237
315
|
) {
|
|
238
316
|
if (elapsedMs <= thresholdMs) return null;
|
|
239
317
|
const cpuMs = context?.cpuMs;
|
|
240
|
-
const
|
|
241
|
-
|
|
318
|
+
const redactorMs = context?.redactorMs;
|
|
319
|
+
const attributed =
|
|
320
|
+
typeof cpuMs === "number" && typeof redactorMs === "number";
|
|
321
|
+
const attribution = attributed
|
|
322
|
+
? attributeWait(elapsedMs, cpuMs, redactorMs)
|
|
323
|
+
: typeof cpuMs === "number"
|
|
242
324
|
? `, and used ${formatSeconds(cpuMs)}s of CPU. ` +
|
|
243
325
|
"Only the CPU share is work every affected call repeats; the rest was spent waiting, on a busy machine or on something this hook called."
|
|
244
326
|
: ". Wall-clock alone cannot separate the sanitizer's own work from a busy machine.";
|
|
327
|
+
const timings = attributed
|
|
328
|
+
? "all three timings"
|
|
329
|
+
: typeof cpuMs === "number"
|
|
330
|
+
? "both timings"
|
|
331
|
+
: "timing";
|
|
245
332
|
return (
|
|
246
333
|
`agent-sanitizer PERFORMANCE: the ${hookName} hook took ` +
|
|
247
334
|
`${formatSeconds(elapsedMs)}s${formatContextSuffix(context)}, over its ${formatSeconds(thresholdMs)}s budget${attribution} ` +
|
|
248
|
-
`Tell the user, and suggest they report it at ${ISSUE_URL} with the hook name and
|
|
249
|
-
`${typeof cpuMs === "number" ? "both timings" : "timing"}.`
|
|
335
|
+
`Tell the user, and suggest they report it at ${ISSUE_URL} with the hook name and ${timings}.`
|
|
250
336
|
);
|
|
251
337
|
}
|
|
252
338
|
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
* daemon could not vet input.
|
|
19
19
|
*/
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
chargeRedactorRoundTrip,
|
|
23
|
+
excludeProvisioning,
|
|
24
|
+
} from "./hook-timing.mjs";
|
|
22
25
|
import { existsSync, lstatSync } from "node:fs";
|
|
23
26
|
import { createConnection } from "node:net";
|
|
24
27
|
import { tmpdir, userInfo } from "node:os";
|
|
@@ -435,9 +438,9 @@ export async function redactViaDaemon(text, opts = {}) {
|
|
|
435
438
|
connect = connectAndRequest,
|
|
436
439
|
spawn: spawnFn = spawnDaemon,
|
|
437
440
|
waitForSocket: waitFn = waitForSocket,
|
|
438
|
-
// The clock the provisioning
|
|
439
|
-
//
|
|
440
|
-
// rather than real time.
|
|
441
|
+
// The clock the provisioning and round-trip charges are measured on;
|
|
442
|
+
// injectable alongside the seams they bracket, since a stubbed wait or
|
|
443
|
+
// connect advances a test clock rather than real time.
|
|
441
444
|
now = Date.now,
|
|
442
445
|
} = opts;
|
|
443
446
|
// Remaining shared budget in ms, or undefined when no budget was threaded (the
|
|
@@ -487,9 +490,20 @@ export async function redactViaDaemon(text, opts = {}) {
|
|
|
487
490
|
);
|
|
488
491
|
return result;
|
|
489
492
|
};
|
|
493
|
+
// Every dial is charged to the redactor share, so the slow-hook notice can
|
|
494
|
+
// place the wait in this call rather than guess at it: the daemon is a
|
|
495
|
+
// separate process, so its cost appears in neither this process's
|
|
496
|
+
// getrusage(RUSAGE_SELF) nor — being long-lived and never reaped here — in
|
|
497
|
+
// RUSAGE_CHILDREN. The round trip is the only measurable stand-in.
|
|
498
|
+
/** @param {number | undefined} deadlineMs @returns {Promise<RedactResponse|null>} */
|
|
499
|
+
const dial = (deadlineMs) =>
|
|
500
|
+
chargeRedactorRoundTrip(
|
|
501
|
+
() => connect(socketPath, request, deadlineMs),
|
|
502
|
+
now,
|
|
503
|
+
);
|
|
490
504
|
try {
|
|
491
505
|
// undefined remaining → connectAndRequest's own default request deadline.
|
|
492
|
-
return validate(await
|
|
506
|
+
return validate(await dial(remainingMs()));
|
|
493
507
|
} catch (err) {
|
|
494
508
|
if (!isRespawnable(err)) throw failClosed(err);
|
|
495
509
|
// Socket absent or dead: (re)spawn the daemon, wait for it, retry exactly once
|
|
@@ -520,7 +534,7 @@ export async function redactViaDaemon(text, opts = {}) {
|
|
|
520
534
|
);
|
|
521
535
|
if (budgetSpent()) throw outOfBudget("after redactor respawn");
|
|
522
536
|
try {
|
|
523
|
-
return validate(await
|
|
537
|
+
return validate(await dial(remainingMs()));
|
|
524
538
|
} catch (err2) {
|
|
525
539
|
throw failClosed(err2);
|
|
526
540
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.45.
|
|
3
|
+
"version": "2.45.2",
|
|
4
4
|
"description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,22 @@ export function formatSeconds(ms: number): string;
|
|
|
21
21
|
* @returns {string}
|
|
22
22
|
*/
|
|
23
23
|
export function formatBytes(bytes: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Run `work` — one redactor round trip — charging its duration to the redactor
|
|
26
|
+
* share, so a run that spent its second inside a redaction call is told apart
|
|
27
|
+
* from one that spent it anywhere else. Charged in a `finally`, since a round
|
|
28
|
+
* trip that THROWS (a stall that hit its deadline) is the one that spent the
|
|
29
|
+
* most.
|
|
30
|
+
*
|
|
31
|
+
* Unlike {@link excludeProvisioning} this only attributes; the time stays in the
|
|
32
|
+
* hook's wall-clock, because a slow redaction is a per-call cost the user waits
|
|
33
|
+
* for.
|
|
34
|
+
* @template T
|
|
35
|
+
* @param {() => Promise<T>} work
|
|
36
|
+
* @param {() => number} [now] injectable clock, for tests
|
|
37
|
+
* @returns {Promise<T>}
|
|
38
|
+
*/
|
|
39
|
+
export function chargeRedactorRoundTrip<T>(work: () => Promise<T>, now?: () => number): Promise<T>;
|
|
24
40
|
/**
|
|
25
41
|
* Run `work`, charging its whole duration to provisioning so no timer running
|
|
26
42
|
* across it counts that time. Charged in a `finally`, so a provisioning step
|
|
@@ -46,22 +62,25 @@ export function excludeProvisioning<T>(work: () => Promise<T>, now?: () => numbe
|
|
|
46
62
|
* so far MINUS any provisioning charged in the meantime, and may be called more
|
|
47
63
|
* than once.
|
|
48
64
|
*
|
|
49
|
-
* `wallMs` is what the user waited
|
|
50
|
-
* computed
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
65
|
+
* `wallMs` is what the user waited, `cpuMs` is what this process actually
|
|
66
|
+
* computed, and `redactorMs` is what it spent inside redactor round trips
|
|
67
|
+
* ({@link chargeRedactorRoundTrip}). All three are needed to say where a slow
|
|
68
|
+
* run's time went — see the module header for the report that read a contended
|
|
69
|
+
* host as a sanitizer bug.
|
|
70
|
+
*
|
|
71
|
+
* Every reader counts only what was charged since this timer started, so an
|
|
72
|
+
* earlier run's cold start cannot pay down a later run's real cost, and an
|
|
73
|
+
* earlier run's round trip cannot be blamed on this one. A provisioning window
|
|
74
|
+
* that straddles the timer's start would otherwise be able to subtract more than
|
|
75
|
+
* the timer has measured, so the results are floored at 0.
|
|
58
76
|
* @param {() => number} [now] injectable clock, for tests
|
|
59
77
|
* @param {() => number} [cpuNow] injectable CPU clock, for tests
|
|
60
|
-
* @returns {{ wallMs: () => number, cpuMs: () => number }}
|
|
78
|
+
* @returns {{ wallMs: () => number, cpuMs: () => number, redactorMs: () => number }}
|
|
61
79
|
*/
|
|
62
80
|
export function startHookTimer(now?: () => number, cpuNow?: () => number): {
|
|
63
81
|
wallMs: () => number;
|
|
64
82
|
cpuMs: () => number;
|
|
83
|
+
redactorMs: () => number;
|
|
65
84
|
};
|
|
66
85
|
/**
|
|
67
86
|
* The model-facing line for a hook that overran the budget, or null when it did
|
|
@@ -70,14 +89,16 @@ export function startHookTimer(now?: () => number, cpuNow?: () => number): {
|
|
|
70
89
|
* is asked to relay the numbers, since the operator is the one who can file it.
|
|
71
90
|
*
|
|
72
91
|
* With `context.cpuMs` in hand the line says which share of the wait was the
|
|
73
|
-
* sanitizer computing
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
92
|
+
* sanitizer computing, and with `context.redactorMs` too it names the window the
|
|
93
|
+
* time went into ({@link attributeWait}). Without them the line says that it cannot
|
|
94
|
+
* tell, rather than asserting an attribution nothing measured: a wall-clock
|
|
95
|
+
* overrun on a loaded host is the common case, and blaming it on the sanitizer
|
|
96
|
+
* sends the operator hunting a per-call cost that does not exist.
|
|
97
|
+
*
|
|
98
|
+
* A CPU figure alone cannot pick a cause, so with only that the clause names
|
|
99
|
+
* candidates and commits to none. A hook that blocks on a dead socket inside a
|
|
100
|
+
* HOST extension spends no CPU and adds no machine load, so naming either as the
|
|
101
|
+
* cause would be a second wrong guess.
|
|
81
102
|
* @param {string} hookName
|
|
82
103
|
* @param {number} elapsedMs
|
|
83
104
|
* @param {number} [thresholdMs]
|
|
@@ -174,12 +195,15 @@ export function reportSlowHook(hookName: string, elapsedMs: number, hookEventNam
|
|
|
174
195
|
* before anyone traced it back here). A hook past the budget therefore says so
|
|
175
196
|
* IN BAND, in the model's context, where it can be relayed to the operator.
|
|
176
197
|
*
|
|
177
|
-
*
|
|
198
|
+
* THREE numbers, because wall-clock alone cannot say whose cost it is: a hook on
|
|
178
199
|
* a contended host waits far longer than it computes (a 1.1 KB payload and a
|
|
179
200
|
* 235 KB one both reported 7.2s on a loaded 2-vCPU box, against 0.3s of work).
|
|
180
|
-
* So the notice prints
|
|
181
|
-
*
|
|
182
|
-
*
|
|
201
|
+
* So the notice prints, beside the clock, the CPU this process burned and the
|
|
202
|
+
* time it spent inside redactor round trips — the daemon is a separate,
|
|
203
|
+
* long-lived process whose CPU this one cannot see (see {@link processCpuMs}),
|
|
204
|
+
* so the call that waits for it is the only measurable stand-in. The notice
|
|
205
|
+
* GATES on none of them: a hook wedged on a dead redactor socket burns no CPU
|
|
206
|
+
* and is exactly the sanitizer's fault.
|
|
183
207
|
*
|
|
184
208
|
* ONE-TIME PROVISIONING is excluded (see {@link excludeProvisioning}): charging
|
|
185
209
|
* an install to the hook that merely waited it out would make the FIRST call of
|
|
@@ -217,12 +241,14 @@ export const SLOW_PROVISION_THRESHOLD_MS: 60000;
|
|
|
217
241
|
* that made this specific latency report take a manual multi-step
|
|
218
242
|
* investigation to characterize (which tool call, how large a payload) before
|
|
219
243
|
* anyone could act on it.
|
|
220
|
-
* `cpuMs` is the run's own processor time
|
|
221
|
-
*
|
|
244
|
+
* `cpuMs` is the run's own processor time and `redactorMs` the wall-clock it
|
|
245
|
+
* spent inside redactor round trips (both from {@link startHookTimer}); absent
|
|
246
|
+
* when the caller has no way to measure them, which is what the shell port of
|
|
222
247
|
* this module reports.
|
|
223
248
|
*/
|
|
224
249
|
export type SlowHookContext = {
|
|
225
250
|
payloadBytes?: number | null;
|
|
226
251
|
tool?: string | null;
|
|
227
252
|
cpuMs?: number | null;
|
|
253
|
+
redactorMs?: number | null;
|
|
228
254
|
};
|