pi-claude-supervisor 0.9.2 → 0.10.0
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 +16 -0
- package/README.cn.md +20 -3
- package/README.md +25 -3
- package/docs/architecture.md +127 -12
- package/docs/autonomy-target.md +2 -1
- package/docs/implementation-review.md +2 -1
- package/docs/optimization-roadmap.md +437 -0
- package/docs/testing.md +5 -3
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/decision-worker.ts +105 -5
- package/src/hooks/relay.ts +28 -15
- package/src/hooks/server.ts +4 -3
- package/src/hooks/settings.ts +7 -2
- package/src/hooks/types.ts +11 -3
- package/src/index.ts +3 -1
- package/src/policy.ts +611 -2
- package/src/supervisor.ts +290 -38
- package/src/verifier.ts +23 -12
- package/src/worker/environment.ts +7 -0
- package/src/worker/input-error.ts +20 -0
- package/src/worker/tmux-adapter.ts +207 -14
package/src/decision-worker.ts
CHANGED
|
@@ -89,6 +89,12 @@ export interface DecisionWorkerOptions {
|
|
|
89
89
|
compactionTokens?: number;
|
|
90
90
|
/** Token accounting for every model call made by this session. */
|
|
91
91
|
onUsage?: (usage: PiUsageSample) => void;
|
|
92
|
+
/** A decision request failed transiently and will be asked again after `delayMs`. */
|
|
93
|
+
onRetry?: (event: WorkerEvent, info: { attempt: number; delayMs: number; error: unknown }) => void;
|
|
94
|
+
/** True once a later event has made this one moot: it is then dropped, neither retried nor failed. */
|
|
95
|
+
isSuperseded?: (event: WorkerEvent) => boolean;
|
|
96
|
+
/** A moot event was dropped while its decision was failing (for the audit log). */
|
|
97
|
+
onSuperseded?: (event: WorkerEvent, error: unknown) => void;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export type PiModel = NonNullable<NonNullable<Parameters<typeof createAgentSession>[0]>["model"]>;
|
|
@@ -105,6 +111,58 @@ export const STALE_VERIFICATION_TURNS = 3;
|
|
|
105
111
|
const DEFAULT_DECISION_RETRY_BACKOFF_MS = 5_000;
|
|
106
112
|
const MAX_DECISION_RETRY_BACKOFF_MS = 60_000;
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Errors no amount of waiting fixes: rejected, missing or expired
|
|
116
|
+
* credentials, an exhausted account, a model that does not exist. Matched on
|
|
117
|
+
* words, not bare status codes, which also turn up inside request ids, URLs
|
|
118
|
+
* and wrapped upstream errors.
|
|
119
|
+
*/
|
|
120
|
+
const CONFIGURATION_ERROR = /(?:account|organi[sz]ation)[^\n]{0,40}(?:terminated|disabled|suspended|deactivated)|authentication|unauthori[sz]ed|incorrect api key|token has expired|invalid[_ ]?x?-?api[_ -]?key|api[_ ]key[_ ](?:not[_ ]valid|expired|invalid)|no api key found|security token[^\n]{0,40}invalid|UnrecognizedClient|ExpiredToken|AccessDenied|permission[_ ]denied|permission_error|payment[_ ]required|credit[_ ]balance|insufficient[_ ]quota|billing_error|not_found_error|models?\b[^\n]{0,80}\b(?:not[_ ]found|does not exist|is invalid)|model identifier is invalid|unsupported[_ ]model|invalid[_ ]model/iu;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Errors that pass by themselves: provider load and rate limits (including a
|
|
124
|
+
* quota that resets, whose Gemini message also mentions "billing"), 5xx,
|
|
125
|
+
* gateway and network failures. Based on pi-ai's own retryable list. Anything
|
|
126
|
+
* matching neither list, such as a prompt that is too long, a corrupted
|
|
127
|
+
* session or this worker's own request timeout, fails the same way every time
|
|
128
|
+
* and stays bounded by maxDecisionRetries.
|
|
129
|
+
*/
|
|
130
|
+
const TRANSIENT_ERROR = /overloaded|rate.?limit|too many requests|resource.?exhausted|retry in \d|quota exceeded for metric|exceeded your current quota|\b(?:429|500|502|503|504|524|529)\b|service.?unavailable|temporarily unavailable|server.?error|internal.?error|provider.?returned.?error|network.?error|connection.?(?:error|refused|lost|reset)|other side closed|fetch failed|getaddrinfo|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|upstream.?connect|reset before headers|socket hang up|socket connection was closed|gateway.?time.?out|websocket.?(?:closed|error)|ended without|stream ended before|http2 request did not get a response|you can retry your request|try your request again|please retry your request|try again in|timed? ?out|timeout|terminated|premature close|\b408\b/iu;
|
|
131
|
+
|
|
132
|
+
/** The Decision Worker's own per-request timeout: the model ran the whole budget. */
|
|
133
|
+
const OWN_TIMEOUT = /^Decision Worker [^\n]* timed out after \d+ms$/u;
|
|
134
|
+
|
|
135
|
+
export type DecisionErrorClass = "configuration" | "transient" | "other";
|
|
136
|
+
|
|
137
|
+
export function classifyDecisionError(error: unknown): DecisionErrorClass {
|
|
138
|
+
if (!(error instanceof Error)) return "other";
|
|
139
|
+
if (error.name === "DecisionWorkerConfigError") return "configuration";
|
|
140
|
+
if (OWN_TIMEOUT.test(error.message)) return "other";
|
|
141
|
+
// Judge the provider's own text, not this worker's "… model request failed:" wrapper.
|
|
142
|
+
const text = error.message.replace(/^[^\n:]*model request failed: /u, "");
|
|
143
|
+
if (CONFIGURATION_ERROR.test(text)) return "configuration";
|
|
144
|
+
// A provider's error turn (pi-ai reports it as a stop reason, not a throw).
|
|
145
|
+
if (error.name === "DecisionWorkerApiError" && TRANSIENT_ERROR.test(text)) return "transient";
|
|
146
|
+
// A thrown transport failure before any provider answered.
|
|
147
|
+
if (/fetch failed|getaddrinfo|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|socket hang up|network.?error/iu.test(text)) return "transient";
|
|
148
|
+
return "other";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isDecisionConfigurationError(error: unknown): boolean {
|
|
152
|
+
return classifyDecisionError(error) === "configuration";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function configurationError(error: Error): Error {
|
|
156
|
+
const wrapped = new Error(`Decision Worker configuration error (credentials, billing or model): ${error.message}`, { cause: error });
|
|
157
|
+
wrapped.name = "DecisionWorkerConfigError";
|
|
158
|
+
return wrapped;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Marks an error thrown while the Supervisor applied an action, as opposed to one from the model. */
|
|
162
|
+
export function isDecisionActionError(error: unknown): boolean {
|
|
163
|
+
return Boolean(error && typeof error === "object" && (error as { decisionActionFailed?: unknown }).decisionActionFailed === true);
|
|
164
|
+
}
|
|
165
|
+
|
|
108
166
|
/**
|
|
109
167
|
* A persistent Pi SDK session used only for supervision decisions.
|
|
110
168
|
* It has read-only repository tools and can request typed actions, but it
|
|
@@ -122,6 +180,7 @@ export class PiDecisionWorker implements DecisionWorkerLike {
|
|
|
122
180
|
readonly #timeoutMs: number;
|
|
123
181
|
/** Ends a startup retry backoff early; set only while one is waiting. */
|
|
124
182
|
#wakeStartupBackoff?: () => void;
|
|
183
|
+
#wakeRetryBackoff?: () => void;
|
|
125
184
|
readonly #retryBackoffMs: number;
|
|
126
185
|
readonly #compactionTokens: number;
|
|
127
186
|
/** Set after a compaction; the next primary decision prompt re-sends the startup instructions once. */
|
|
@@ -187,7 +246,7 @@ export class PiDecisionWorker implements DecisionWorkerLike {
|
|
|
187
246
|
if (this.#closed) throw closedDuringStartup();
|
|
188
247
|
// Only a provider error is worth another attempt: a timeout already
|
|
189
248
|
// spent the whole prompt budget and would only multiply it.
|
|
190
|
-
const retryable = error instanceof Error && error.name === "DecisionWorkerApiError";
|
|
249
|
+
const retryable = error instanceof Error && error.name === "DecisionWorkerApiError" && classifyDecisionError(error) !== "configuration";
|
|
191
250
|
if (attempt >= maxRetries || !retryable) {
|
|
192
251
|
try { await this.#options.onStartupFailure?.(error); } catch { /* preserve the original startup failure */ }
|
|
193
252
|
throw error;
|
|
@@ -259,11 +318,34 @@ export class PiDecisionWorker implements DecisionWorkerLike {
|
|
|
259
318
|
} catch (error) {
|
|
260
319
|
if (this.#closed) return;
|
|
261
320
|
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
262
|
-
|
|
321
|
+
// A later event made this one moot: its failure decides nothing, and
|
|
322
|
+
// failing it would park a task whose current turn is still undecided.
|
|
323
|
+
if (this.#dropIfSuperseded(event, error)) return;
|
|
324
|
+
const kind = classifyDecisionError(error);
|
|
325
|
+
// Waiting does not fix credentials, billing or a missing model: park now, with that reason.
|
|
326
|
+
if (kind === "configuration") throw configurationError(error as Error);
|
|
327
|
+
// A transient provider or network outage on a completed turn is
|
|
328
|
+
// waited out rather than parking the task: nobody is there to resume
|
|
329
|
+
// it, and the Supervisor's idle watchdog verifies the Worker's
|
|
330
|
+
// finished work if no decision ever lands. Nothing like that backs up
|
|
331
|
+
// any other event: a permission request blocks the Worker mid-turn
|
|
332
|
+
// (on some transports nothing else answers it), and after an exit
|
|
333
|
+
// only this decision starts verification. Those, a turn a later event
|
|
334
|
+
// has made moot, and anything that fails the same way on every
|
|
335
|
+
// attempt stay bounded.
|
|
336
|
+
const waitOut = kind === "transient" && event.type === "turn_completed";
|
|
337
|
+
if (!waitOut && attempt >= maxRetries) throw error;
|
|
263
338
|
attempt += 1;
|
|
264
339
|
// 429/529 overloads last minutes, not seconds: 15s, 45s, then 60s.
|
|
265
|
-
|
|
340
|
+
const delayMs = Math.min(MAX_DECISION_RETRY_BACKOFF_MS, this.#retryBackoffMs * 3 ** Math.min(attempt, 10));
|
|
341
|
+
try { this.#options.onRetry?.(event, { attempt, delayMs, error }); } catch { /* observability only */ }
|
|
342
|
+
await new Promise<void>((resolveWait) => {
|
|
343
|
+
const timer = setTimeout(() => { this.#wakeRetryBackoff = undefined; resolveWait(); }, delayMs);
|
|
344
|
+
// close() wakes this wait, so a stop does not sit out the backoff.
|
|
345
|
+
this.#wakeRetryBackoff = () => { clearTimeout(timer); this.#wakeRetryBackoff = undefined; resolveWait(); };
|
|
346
|
+
});
|
|
266
347
|
if (!this.#session || this.#closed) return;
|
|
348
|
+
if (this.#dropIfSuperseded(event, error)) return;
|
|
267
349
|
continue;
|
|
268
350
|
}
|
|
269
351
|
if (this.#closed) return;
|
|
@@ -285,8 +367,14 @@ export class PiDecisionWorker implements DecisionWorkerLike {
|
|
|
285
367
|
continue;
|
|
286
368
|
}
|
|
287
369
|
// An action handler may stop or close the session. Do not replay an
|
|
288
|
-
// already-decoded action if the handler itself fails
|
|
289
|
-
|
|
370
|
+
// already-decoded action if the handler itself fails, and do not let
|
|
371
|
+
// its failure read as a model failure.
|
|
372
|
+
try {
|
|
373
|
+
await this.#options.onAction(action, event);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error && typeof error === "object") (error as { decisionActionFailed?: boolean }).decisionActionFailed = true;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
290
378
|
await this.#maybeCompact();
|
|
291
379
|
return;
|
|
292
380
|
}
|
|
@@ -327,11 +415,23 @@ export class PiDecisionWorker implements DecisionWorkerLike {
|
|
|
327
415
|
return Boolean(this.#options.sessionFile && this.#sessionFile === this.#options.sessionFile);
|
|
328
416
|
}
|
|
329
417
|
|
|
418
|
+
#dropIfSuperseded(event: WorkerEvent, error: unknown): boolean {
|
|
419
|
+
if (this.#options.isSuperseded?.(event) !== true) return false;
|
|
420
|
+
try { this.#options.onSuperseded?.(event, error); } catch { /* observability only */ }
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Resolves once every queued decision has finished or given up (shutdown and tests). */
|
|
425
|
+
async settled(): Promise<void> {
|
|
426
|
+
await this.#tail.catch(() => {});
|
|
427
|
+
}
|
|
428
|
+
|
|
330
429
|
async close(): Promise<void> {
|
|
331
430
|
// Do not await #tail here: onAction may be closing the worker from inside
|
|
332
431
|
// the same queued decision, which would otherwise deadlock shutdown.
|
|
333
432
|
this.#closed = true;
|
|
334
433
|
this.#wakeStartupBackoff?.();
|
|
434
|
+
this.#wakeRetryBackoff?.();
|
|
335
435
|
const session = this.#session;
|
|
336
436
|
this.#session = undefined;
|
|
337
437
|
if (session) await session.abort().catch(() => {});
|
package/src/hooks/relay.ts
CHANGED
|
@@ -12,6 +12,14 @@ import { shellQuote } from "../policy.ts";
|
|
|
12
12
|
* with empty stdout, so a relay bug never blocks Claude Code. The fast path
|
|
13
13
|
* (no Supervisor listening for this cwd) never requires "net", matching the
|
|
14
14
|
* expectation that it costs about a millisecond.
|
|
15
|
+
*
|
|
16
|
+
* Routing prefers `CLAUDE_PROJECT_DIR`, which Claude Code sets on every hook
|
|
17
|
+
* command to the directory the session was launched in, over the event's
|
|
18
|
+
* `cwd`, which follows a persisted Bash `cd` into a subdirectory. Routing by
|
|
19
|
+
* `cwd` alone silently drops every later event (Stop, PreToolUse,
|
|
20
|
+
* PermissionRequest) of a Worker that changed directory. A nested Claude
|
|
21
|
+
* sets its own project directory, so it is not routed to its parent's
|
|
22
|
+
* Supervisor by this.
|
|
15
23
|
*/
|
|
16
24
|
export const HOOK_RELAY_SCRIPT = `
|
|
17
25
|
(function () {
|
|
@@ -73,22 +81,26 @@ export const HOOK_RELAY_SCRIPT = `
|
|
|
73
81
|
var hookDir = process.env.PI_CLAUDE_SUPERVISOR_HOOK_DIR
|
|
74
82
|
|| (/[\\/]relay\.js$/.test(scriptPath) ? path.dirname(scriptPath) : "")
|
|
75
83
|
|| path.join(os.homedir(), ".pi", "agent", "claude-supervisor", "hooks");
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} catch (e) {
|
|
89
|
-
return;
|
|
84
|
+
function socketFor(dir) {
|
|
85
|
+
if (typeof dir !== "string" || !path.isAbsolute(dir)) return undefined;
|
|
86
|
+
var canonical = dir;
|
|
87
|
+
try {
|
|
88
|
+
canonical = fs.realpathSync(dir);
|
|
89
|
+
} catch (e) { /* fall back to the raw path */ }
|
|
90
|
+
var candidate = path.join(hookDir, "by-cwd", crypto.createHash("sha256").update(canonical).digest("hex"));
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(candidate).isSocket() ? { socketPath: candidate, routeCwd: canonical } : undefined;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
|
-
|
|
97
|
+
|
|
98
|
+
// Fast path: no Supervisor owns the project directory or the cwd. Check
|
|
99
|
+
// before requiring "net" so the common case (no hooks configured / no
|
|
100
|
+
// owning Supervisor) is cheap.
|
|
101
|
+
var route = socketFor(process.env.CLAUDE_PROJECT_DIR) || socketFor(event.cwd);
|
|
102
|
+
if (!route) return;
|
|
103
|
+
var socketPath = route.socketPath;
|
|
92
104
|
|
|
93
105
|
var BLOCKING_EVENTS = ${JSON.stringify([...BLOCKING_HOOK_EVENTS])};
|
|
94
106
|
var blocking = BLOCKING_EVENTS.indexOf(event.hook_event_name) !== -1;
|
|
@@ -97,6 +109,7 @@ export const HOOK_RELAY_SCRIPT = `
|
|
|
97
109
|
pid: process.pid,
|
|
98
110
|
ppid: process.ppid,
|
|
99
111
|
tmuxPane: process.env.TMUX_PANE,
|
|
112
|
+
routeCwd: route.routeCwd,
|
|
100
113
|
event: event,
|
|
101
114
|
}) + "\\n";
|
|
102
115
|
|
package/src/hooks/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer, type Server, type Socket } from "node:net";
|
|
3
3
|
import { chmod, lstat, mkdir, readdir, readlink, realpath, rename, rm, symlink, unlink } from "node:fs/promises";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { CLAUDE_HOOK_EVENT_NAMES, type ClaudeHookEvent, type ClaudeHookEventName, type HookEventSource, type HookRelayReply, type HookRelayRequest } from "./types.ts";
|
|
7
7
|
|
|
@@ -33,7 +33,7 @@ export function hookSocketRuntimeDirectory(env: NodeJS.ProcessEnv = process.env)
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* One socket per Pi process, with a per-cwd symlink under `by-cwd/` so a
|
|
36
|
-
* relay only needs the
|
|
36
|
+
* relay only needs the task directory to find its Supervisor. See
|
|
37
37
|
* src/hooks/types.ts for the full routing contract.
|
|
38
38
|
*/
|
|
39
39
|
export class HookServer implements HookEventSource {
|
|
@@ -197,7 +197,7 @@ export class HookServer implements HookEventSource {
|
|
|
197
197
|
try {
|
|
198
198
|
const request = parseRequest(line);
|
|
199
199
|
if (request) {
|
|
200
|
-
const handler = this.#handlers.get(hashCwd(await canonicalize(request.event.cwd)));
|
|
200
|
+
const handler = this.#handlers.get(hashCwd(await canonicalize(request.routeCwd ?? request.event.cwd)));
|
|
201
201
|
if (handler) reply = (await handler(request)) ?? {};
|
|
202
202
|
}
|
|
203
203
|
} catch {
|
|
@@ -243,6 +243,7 @@ function parseRequest(line: string): HookRelayRequest | undefined {
|
|
|
243
243
|
const request = value as Partial<HookRelayRequest>;
|
|
244
244
|
if (request.version !== 1) return undefined;
|
|
245
245
|
if (typeof request.pid !== "number" || typeof request.ppid !== "number") return undefined;
|
|
246
|
+
if (request.routeCwd !== undefined && (typeof request.routeCwd !== "string" || !isAbsolute(request.routeCwd))) return undefined;
|
|
246
247
|
const event = request.event as Partial<ClaudeHookEvent> | undefined;
|
|
247
248
|
if (!event || typeof event !== "object") return undefined;
|
|
248
249
|
if (typeof event.cwd !== "string" || !event.cwd) return undefined;
|
package/src/hooks/settings.ts
CHANGED
|
@@ -13,15 +13,20 @@ interface HookCommandEntry {
|
|
|
13
13
|
|
|
14
14
|
export interface HookSettingsDocument {
|
|
15
15
|
hooks: Record<ClaudeHookEventName, Array<{ hooks: HookCommandEntry[] }>>;
|
|
16
|
+
/**
|
|
17
|
+
* Off for a supervised session: a suggestion is ghost text in the input
|
|
18
|
+
* box, which reads as leftover input and holds every send back.
|
|
19
|
+
*/
|
|
20
|
+
promptSuggestionEnabled: false;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
/** A Claude Code settings document
|
|
23
|
+
/** A Claude Code settings document for a supervised session: runs `relayCommand` for every hook event the relay supports. */
|
|
19
24
|
export function hookSettingsDocument(relayCommand: string): HookSettingsDocument {
|
|
20
25
|
const hooks = {} as HookSettingsDocument["hooks"];
|
|
21
26
|
for (const name of HOOK_EVENT_NAMES) {
|
|
22
27
|
hooks[name] = [{ hooks: [{ type: "command", command: relayCommand, timeout: HOOK_TIMEOUT_SECONDS }] }];
|
|
23
28
|
}
|
|
24
|
-
return { hooks };
|
|
29
|
+
return { hooks, promptSuggestionEnabled: false };
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/** Writes a standalone settings file (e.g. for `--settings <path>` on an owned interactive session). */
|
package/src/hooks/types.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface HookRelayRequest {
|
|
|
59
59
|
ppid: number;
|
|
60
60
|
/** Inherited from Claude's environment when it runs inside tmux. */
|
|
61
61
|
tmuxPane?: string;
|
|
62
|
+
/**
|
|
63
|
+
* The canonical directory the relay routed by: Claude's `CLAUDE_PROJECT_DIR`
|
|
64
|
+
* when a Supervisor owns it, else the event's `cwd`. The server routes by
|
|
65
|
+
* this when present, so a Worker that `cd`s into a subdirectory stays
|
|
66
|
+
* supervised. Absent from older relays, which routed by `cwd`.
|
|
67
|
+
*/
|
|
68
|
+
routeCwd?: string;
|
|
62
69
|
event: ClaudeHookEvent;
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -84,10 +91,11 @@ export const HOOK_TIMEOUT_SECONDS = 180;
|
|
|
84
91
|
/**
|
|
85
92
|
* Socket routing: one socket per Pi process under `<stateDir>/hooks/`, plus a
|
|
86
93
|
* per-cwd symlink `<stateDir>/hooks/by-cwd/<sha256(cwd)>` created when the
|
|
87
|
-
* Supervisor takes the cwd lease, so a relay only needs the
|
|
88
|
-
*
|
|
94
|
+
* Supervisor takes the cwd lease, so a relay only needs the task directory
|
|
95
|
+
* (Claude's `CLAUDE_PROJECT_DIR`, else the event's `cwd`) to find its
|
|
96
|
+
* supervisor and a directory with no supervisor is a no-op.
|
|
89
97
|
*/
|
|
90
98
|
export interface HookEventSource {
|
|
91
|
-
/** Route every event whose canonical cwd matches to this handler until unsubscribed. */
|
|
99
|
+
/** Route every event whose canonical routing directory (`routeCwd`, else `event.cwd`) matches to this handler until unsubscribed. */
|
|
92
100
|
subscribe(cwd: string, handler: (request: HookRelayRequest) => Promise<HookRelayReply | undefined>): Promise<() => Promise<void>>;
|
|
93
101
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { TmuxWorkerAdapter, attachCommand, sweepDeadTmuxSockets } from "./worker
|
|
|
12
12
|
import { Supervisor, extendedDeadlineMs, type DecisionSessionClosedInfo, type HumanInterventionNotice, type SupervisorProgress, type SupervisorTokenUsage } from "./supervisor.ts";
|
|
13
13
|
import { evaluateCommand } from "./policy.ts";
|
|
14
14
|
import { HumanWebhookNotifier } from "./notifications.ts";
|
|
15
|
-
import { autoInstallHooks, autonomyDefaults, closeWorkerOnCompletion, deadlineGraceMs, deadlineMs, deadlineWarningMs, decisionCompactionTokens, decisionModel, decisionSessionRetentionDays, eventLogMaxBytes, formatDurationMs, loadSupervisorEnvironment, noOutputTimeoutMs, parseDurationMs, progressHeartbeatMs, reviewTimeoutMs, reviewerModel, tmuxMode, workerAutocompactTokens, workerMcpConfigPath, workerModel } from "./config.ts";
|
|
15
|
+
import { autoInstallHooks, autonomyDefaults, closeWorkerOnCompletion, deadlineGraceMs, deadlineMs, deadlineWarningMs, decisionCompactionTokens, decisionModel, decisionSessionRetentionDays, eventLogMaxBytes, formatDurationMs, humanIdleResumeMs, loadSupervisorEnvironment, noOutputTimeoutMs, parseDurationMs, progressHeartbeatMs, reviewTimeoutMs, reviewerModel, tmuxMode, workerAutocompactTokens, workerMcpConfigPath, workerModel } from "./config.ts";
|
|
16
16
|
import { DecisionSessionStore, type DecisionSessionRecord } from "./decision-session-store.ts";
|
|
17
17
|
import { CwdLeaseStore, type CwdLeaseHandle, leaseOwnerLive, pathsOverlap, workerIdentity } from "./cwd-lease.ts";
|
|
18
18
|
import { normalizeTaskSpec } from "./acceptance.ts";
|
|
@@ -478,6 +478,7 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
|
|
|
478
478
|
deadlineGraceMs: deadlineGraceMs(),
|
|
479
479
|
deadlineWarningMs: deadlineWarningMs(),
|
|
480
480
|
noOutputTimeoutMs: noOutputTimeoutMs(),
|
|
481
|
+
humanIdleResumeMs: humanIdleResumeMs(),
|
|
481
482
|
hookSource: interactive ? hookServer : undefined,
|
|
482
483
|
hookSettingsPath,
|
|
483
484
|
keepWorkerOnCompletion: interactive ? !closeWorkerOnCompletion() : undefined,
|
|
@@ -825,6 +826,7 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
|
|
|
825
826
|
deadlineGraceMs: deadlineGraceMs(),
|
|
826
827
|
deadlineWarningMs: deadlineWarningMs(),
|
|
827
828
|
noOutputTimeoutMs: record.noOutputTimeoutMs,
|
|
829
|
+
humanIdleResumeMs: humanIdleResumeMs(),
|
|
828
830
|
startedAt: record.startedAt,
|
|
829
831
|
baseCommit: record.baseCommit,
|
|
830
832
|
baseBranch: record.baseBranch,
|