@norman-else/dsh-claude 0.1.41 → 0.1.42
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/README.md +3 -1
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +15 -1
- package/lib/client.js +658 -8
- package/lib/client.js.map +1 -1
- package/lib/{events-B-FPMzI7.mjs → events-oovRTmX7.mjs} +5 -2
- package/lib/events-oovRTmX7.mjs.map +1 -0
- package/lib/index.d.mts +1 -0
- package/lib/index.mjs +658 -40
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BV42EKkB.mjs → presenters-BVWj7u0a.mjs} +2 -2
- package/lib/{presenters-BV42EKkB.mjs.map → presenters-BVWj7u0a.mjs.map} +1 -1
- package/lib/{preset-installer-yRGfkGjd.mjs → preset-installer-JUktnfwS.mjs} +2 -2
- package/lib/{preset-installer-yRGfkGjd.mjs.map → preset-installer-JUktnfwS.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +1 -1
- package/lib/events-B-FPMzI7.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { A as CLAUDE_REPOSITORY_FEEDBACK_PATH, B as DEFAULT_CLAUDE_RENDER_MODE, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_PROSE_MODES, E as CLAUDE_PROMPT_REFINE_PATH, F as CLAUDE_REWIND_PATH, G as isClaudeRenderMode, H as TASK_TOOL_NAMES, I as CLAUDE_UPDATE_CHECK_PATH, L as CLAUDE_UPDATE_PATH, M as CLAUDE_REPOSITORY_SETUP_PATH, N as CLAUDE_REPOSITORY_STATUS_PATH, O as CLAUDE_RENDER_MODES, P as CLAUDE_REVIEW_COMMENT_PATH, R as CLAUDE_USAGE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_PROMPT_NAME_PATH, U as isClaudeAlertMode, W as isClaudeProseMode, _ as CLAUDE_CODE_PROVIDER_IDS, a as latestClaudeTasks, b as CLAUDE_GLOBAL_SETTINGS_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ALERT_MODES, g as CLAUDE_CODE_PROVIDER, h as CLAUDE_CODE_PRESET_ID, i as latestClaudeSessionBinding, j as CLAUDE_REPOSITORY_FILE_PATH, k as CLAUDE_REPOSITORY_ACTION_PATH, l as redactText, m as CLAUDE_CLIENT_DIAGNOSTICS_PATH, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_ASK_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_DOCTOR_PATH, w as CLAUDE_PROMPTS_PATH, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as DEFAULT_CLAUDE_PROSE_MODE } from "./events-oovRTmX7.mjs";
|
|
2
|
+
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BVWj7u0a.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-JUktnfwS.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -1840,6 +1840,21 @@ async function withTimeout(operation, timeoutMs, label) {
|
|
|
1840
1840
|
if (timer !== void 0) clearTimeout(timer);
|
|
1841
1841
|
}
|
|
1842
1842
|
}
|
|
1843
|
+
async function withAbort(operation, signal) {
|
|
1844
|
+
if (signal === void 0) return operation;
|
|
1845
|
+
if (signal.aborted) throw abortFailure();
|
|
1846
|
+
let abortListener;
|
|
1847
|
+
try {
|
|
1848
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
1849
|
+
abortListener = () => {
|
|
1850
|
+
reject(abortFailure());
|
|
1851
|
+
};
|
|
1852
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
1853
|
+
})]);
|
|
1854
|
+
} finally {
|
|
1855
|
+
if (abortListener !== void 0) signal.removeEventListener("abort", abortListener);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1843
1858
|
/** The uuid of one main-chain transcript entry, or undefined for anything a
|
|
1844
1859
|
* rewind must not fork at: stream partials, results, and sidechain traffic. */
|
|
1845
1860
|
function chainEntryUuid(message) {
|
|
@@ -1895,6 +1910,15 @@ var ClaudeSupervisor = class {
|
|
|
1895
1910
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1896
1911
|
#disposed = false;
|
|
1897
1912
|
#admissionGate = Promise.resolve();
|
|
1913
|
+
/** FIFO user turns live outside the gate while capacity-blocked, so
|
|
1914
|
+
* best-effort metadata can still enter the serialized path and fail. */
|
|
1915
|
+
#turnAdmissions = [];
|
|
1916
|
+
#metadataAdmissions = /* @__PURE__ */ new Set();
|
|
1917
|
+
#admissionDrainScheduled = false;
|
|
1918
|
+
/** Prevent a capacity change between a failed attempt and parking the head
|
|
1919
|
+
* from becoming a lost wake-up. */
|
|
1920
|
+
#admissionRevision = 0;
|
|
1921
|
+
#blockedAdmissionRevision;
|
|
1898
1922
|
constructor(dependencies) {
|
|
1899
1923
|
this.#runtime = dependencies.runtime;
|
|
1900
1924
|
this.#approval = dependencies.approval;
|
|
@@ -1968,28 +1992,140 @@ var ClaudeSupervisor = class {
|
|
|
1968
1992
|
runTurn(request) {
|
|
1969
1993
|
const interruption = this.#interruptions.get(request.agent.id);
|
|
1970
1994
|
if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
|
|
1971
|
-
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
let complete;
|
|
1997
|
+
const completion = new Promise((done) => {
|
|
1998
|
+
complete = done;
|
|
1999
|
+
});
|
|
2000
|
+
const admission = {
|
|
2001
|
+
request,
|
|
2002
|
+
resolve,
|
|
2003
|
+
reject,
|
|
2004
|
+
delivered: false,
|
|
2005
|
+
admitting: false,
|
|
2006
|
+
waitedForCapacity: false,
|
|
2007
|
+
cancellation: new AbortController(),
|
|
2008
|
+
completion,
|
|
2009
|
+
complete: () => {
|
|
2010
|
+
complete?.();
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
if (request.signal !== void 0) {
|
|
2014
|
+
const abortListener = () => {
|
|
2015
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2016
|
+
else if (admission.waitedForCapacity && !admission.delivered) {
|
|
2017
|
+
admission.delivered = true;
|
|
2018
|
+
admission.reject(abortFailure());
|
|
2019
|
+
}
|
|
2020
|
+
this.#admissionRevision += 1;
|
|
2021
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2022
|
+
this.#scheduleTurnAdmissions();
|
|
2023
|
+
};
|
|
2024
|
+
admission.abortListener = abortListener;
|
|
2025
|
+
request.signal.addEventListener("abort", abortListener, { once: true });
|
|
2026
|
+
}
|
|
2027
|
+
this.#turnAdmissions.push(admission);
|
|
2028
|
+
this.#scheduleTurnAdmissions();
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
async #drainTurnAdmissions() {
|
|
2032
|
+
while (this.#turnAdmissions.length > 0) {
|
|
2033
|
+
const admission = this.#turnAdmissions[0];
|
|
2034
|
+
if (signalAborted(admission.request.signal)) {
|
|
2035
|
+
this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const attemptedRevision = this.#admissionRevision;
|
|
2039
|
+
admission.admitting = true;
|
|
2040
|
+
try {
|
|
2041
|
+
const output = await this.#runTurnAdmitted(admission.request, admission.waitedForCapacity, admission.cancellation.signal);
|
|
2042
|
+
this.#finishTurnAdmission(admission, { output });
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
if (error instanceof ClaudeProcessLimitError && !signalAborted(admission.request.signal) && !admission.cancellation.signal.aborted && !this.#disposed) {
|
|
2045
|
+
admission.admitting = false;
|
|
2046
|
+
admission.waitedForCapacity = true;
|
|
2047
|
+
if (attemptedRevision === this.#admissionRevision) {
|
|
2048
|
+
this.#blockedAdmissionRevision = attemptedRevision;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
this.#finishTurnAdmission(admission, { error });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
#finishTurnAdmission(admission, outcome) {
|
|
2058
|
+
if (this.#turnAdmissions[0] === admission) this.#turnAdmissions.shift();
|
|
2059
|
+
else {
|
|
2060
|
+
const index = this.#turnAdmissions.indexOf(admission);
|
|
2061
|
+
if (index >= 0) this.#turnAdmissions.splice(index, 1);
|
|
2062
|
+
}
|
|
2063
|
+
admission.admitting = false;
|
|
2064
|
+
if (admission.request.signal !== void 0 && admission.abortListener !== void 0) admission.request.signal.removeEventListener("abort", admission.abortListener);
|
|
2065
|
+
if (!admission.delivered) {
|
|
2066
|
+
admission.delivered = true;
|
|
2067
|
+
if ("error" in outcome) admission.reject(outcome.error);
|
|
2068
|
+
else admission.resolve(outcome.output);
|
|
2069
|
+
}
|
|
2070
|
+
admission.complete();
|
|
2071
|
+
}
|
|
2072
|
+
#scheduleTurnAdmissions() {
|
|
2073
|
+
if (this.#admissionDrainScheduled || this.#turnAdmissions.length === 0 || this.#blockedAdmissionRevision === this.#admissionRevision) return;
|
|
2074
|
+
this.#admissionDrainScheduled = true;
|
|
2075
|
+
const operation = this.#admissionGate.then(() => this.#drainTurnAdmissions());
|
|
1972
2076
|
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
1973
|
-
|
|
2077
|
+
const finished = () => {
|
|
2078
|
+
this.#admissionDrainScheduled = false;
|
|
2079
|
+
this.#scheduleTurnAdmissions();
|
|
2080
|
+
};
|
|
2081
|
+
operation.then(finished, finished);
|
|
1974
2082
|
}
|
|
1975
|
-
async #runTurnAdmitted(request) {
|
|
2083
|
+
async #runTurnAdmitted(request, abortDuringAdmission, cancellationSignal) {
|
|
1976
2084
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
2085
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1977
2086
|
if (signalAborted(request.signal)) throw abortFailure();
|
|
1978
2087
|
const sessionId = request.agent.id;
|
|
1979
2088
|
let entry = this.#entries.get(sessionId);
|
|
2089
|
+
let createdForRequest;
|
|
2090
|
+
const throwIfUnavailable = async () => {
|
|
2091
|
+
const failure = this.#disposed ? /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed") : cancellationSignal.aborted || abortDuringAdmission && signalAborted(request.signal) ? abortFailure() : void 0;
|
|
2092
|
+
if (failure === void 0) return;
|
|
2093
|
+
if (createdForRequest !== void 0) {
|
|
2094
|
+
if (this.#entries.get(sessionId) === createdForRequest) this.#entries.delete(sessionId);
|
|
2095
|
+
await this.#disposeEntry(createdForRequest);
|
|
2096
|
+
createdForRequest = void 0;
|
|
2097
|
+
}
|
|
2098
|
+
throw failure;
|
|
2099
|
+
};
|
|
1980
2100
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
1981
2101
|
this.#entries.delete(sessionId);
|
|
1982
2102
|
await this.#disposeEntry(entry);
|
|
2103
|
+
await throwIfUnavailable();
|
|
1983
2104
|
entry = void 0;
|
|
1984
2105
|
}
|
|
1985
2106
|
if (entry === void 0) {
|
|
1986
2107
|
await this.#makeRoom();
|
|
1987
|
-
|
|
2108
|
+
await throwIfUnavailable();
|
|
2109
|
+
try {
|
|
2110
|
+
entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
await throwIfUnavailable();
|
|
2113
|
+
throw error;
|
|
2114
|
+
}
|
|
2115
|
+
createdForRequest = entry;
|
|
2116
|
+
await throwIfUnavailable();
|
|
1988
2117
|
this.#entries.set(sessionId, entry);
|
|
1989
2118
|
}
|
|
1990
2119
|
if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
1991
2120
|
if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
|
|
1992
|
-
|
|
2121
|
+
try {
|
|
2122
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2123
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
await throwIfUnavailable();
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
await throwIfUnavailable();
|
|
1993
2129
|
if (entry.idleTimer !== void 0) {
|
|
1994
2130
|
clearTimeout(entry.idleTimer);
|
|
1995
2131
|
entry.idleTimer = void 0;
|
|
@@ -1998,13 +2134,31 @@ var ClaudeSupervisor = class {
|
|
|
1998
2134
|
if (request.thinkingMode !== entry.thinkingMode || model !== entry.model) {
|
|
1999
2135
|
this.#entries.delete(sessionId);
|
|
2000
2136
|
await this.#disposeEntry(entry);
|
|
2001
|
-
entry =
|
|
2137
|
+
if (createdForRequest === entry) createdForRequest = void 0;
|
|
2138
|
+
await throwIfUnavailable();
|
|
2139
|
+
try {
|
|
2140
|
+
entry = await this.#createEntry(request.agent, model, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
await throwIfUnavailable();
|
|
2143
|
+
throw error;
|
|
2144
|
+
}
|
|
2145
|
+
createdForRequest = entry;
|
|
2146
|
+
await throwIfUnavailable();
|
|
2002
2147
|
this.#entries.set(sessionId, entry);
|
|
2003
|
-
|
|
2148
|
+
try {
|
|
2149
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2150
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
await throwIfUnavailable();
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
2004
2155
|
} else await this.#syncPermissionMode(entry);
|
|
2156
|
+
await throwIfUnavailable();
|
|
2005
2157
|
const promptUuid = randomUUID();
|
|
2006
2158
|
const cursor = currentClaudeActivityCursor(request.agent.session.events);
|
|
2007
|
-
|
|
2159
|
+
const projection = await this.#sidecar.read(sessionId);
|
|
2160
|
+
await throwIfUnavailable();
|
|
2161
|
+
cursor.nextOrdinal = projection.activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
|
|
2008
2162
|
const active = {
|
|
2009
2163
|
agent: request.agent,
|
|
2010
2164
|
cursor,
|
|
@@ -2052,9 +2206,12 @@ var ClaudeSupervisor = class {
|
|
|
2052
2206
|
title: "Claude Code turn cancelled before submission"
|
|
2053
2207
|
});
|
|
2054
2208
|
entry.active = void 0;
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2209
|
+
if (!abortDuringAdmission || createdForRequest === void 0) {
|
|
2210
|
+
entry.state = "idle";
|
|
2211
|
+
entry.lastUsedAt = Date.now();
|
|
2212
|
+
this.#armIdleTimer(entry);
|
|
2213
|
+
}
|
|
2214
|
+
await throwIfUnavailable();
|
|
2058
2215
|
return active.output;
|
|
2059
2216
|
}
|
|
2060
2217
|
if (request.signal !== void 0) {
|
|
@@ -2068,31 +2225,57 @@ var ClaudeSupervisor = class {
|
|
|
2068
2225
|
return active.output;
|
|
2069
2226
|
}
|
|
2070
2227
|
#runMetadata(agent, model, operation) {
|
|
2228
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) return Promise.reject(new ClaudeProcessLimitError(this.#config.maxProcesses));
|
|
2229
|
+
let complete;
|
|
2230
|
+
const admission = {
|
|
2231
|
+
sessionId: agent.id,
|
|
2232
|
+
cancellation: new AbortController(),
|
|
2233
|
+
started: false,
|
|
2234
|
+
completion: new Promise((done) => {
|
|
2235
|
+
complete = done;
|
|
2236
|
+
}),
|
|
2237
|
+
complete: () => {
|
|
2238
|
+
complete?.();
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
this.#metadataAdmissions.add(admission);
|
|
2071
2242
|
const admitted = this.#admissionGate.then(async () => {
|
|
2243
|
+
admission.started = true;
|
|
2072
2244
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
2073
|
-
|
|
2245
|
+
if (admission.cancellation.signal.aborted) throw abortFailure();
|
|
2246
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2247
|
+
const entry = await this.#metadataEntry(agent, model, admission.cancellation.signal);
|
|
2074
2248
|
try {
|
|
2075
|
-
await entry.sdkInitialization;
|
|
2076
|
-
return await this.#control(entry, operation(entry.query, entry), "Claude metadata request");
|
|
2249
|
+
await withAbort(entry.sdkInitialization, admission.cancellation.signal);
|
|
2250
|
+
return await withAbort(this.#control(entry, operation(entry.query, entry), "Claude metadata request"), admission.cancellation.signal);
|
|
2077
2251
|
} finally {
|
|
2078
2252
|
entry.lastUsedAt = Date.now();
|
|
2079
2253
|
if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
|
|
2080
2254
|
}
|
|
2255
|
+
}).finally(() => {
|
|
2256
|
+
this.#finishMetadataAdmission(admission);
|
|
2081
2257
|
});
|
|
2082
2258
|
this.#admissionGate = admitted.then(() => void 0, () => void 0);
|
|
2083
|
-
return admitted;
|
|
2259
|
+
return withAbort(admitted, admission.cancellation.signal);
|
|
2084
2260
|
}
|
|
2085
|
-
async #metadataEntry(agent, model) {
|
|
2261
|
+
async #metadataEntry(agent, model, cancellationSignal) {
|
|
2262
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2086
2263
|
const sessionId = agent.id;
|
|
2087
2264
|
let entry = this.#entries.get(sessionId);
|
|
2088
2265
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
2089
2266
|
this.#entries.delete(sessionId);
|
|
2090
2267
|
await this.#disposeEntry(entry);
|
|
2268
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2091
2269
|
entry = void 0;
|
|
2092
2270
|
}
|
|
2093
2271
|
if (entry === void 0) {
|
|
2094
2272
|
await this.#makeRoom();
|
|
2095
|
-
|
|
2273
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2274
|
+
entry = await this.#createEntry(agent, model, void 0, void 0, cancellationSignal);
|
|
2275
|
+
if (cancellationSignal.aborted) {
|
|
2276
|
+
await this.#disposeEntry(entry);
|
|
2277
|
+
throw abortFailure();
|
|
2278
|
+
}
|
|
2096
2279
|
this.#entries.set(sessionId, entry);
|
|
2097
2280
|
}
|
|
2098
2281
|
if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
@@ -2101,14 +2284,15 @@ var ClaudeSupervisor = class {
|
|
|
2101
2284
|
clearTimeout(entry.idleTimer);
|
|
2102
2285
|
entry.idleTimer = void 0;
|
|
2103
2286
|
}
|
|
2104
|
-
await this.#syncPermissionMode(entry);
|
|
2287
|
+
await withAbort(this.#syncPermissionMode(entry), cancellationSignal);
|
|
2288
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2105
2289
|
return entry;
|
|
2106
2290
|
}
|
|
2107
2291
|
/** Run one SDK control request against a live entry, and discard the entry if
|
|
2108
2292
|
* it does not answer.
|
|
2109
2293
|
*
|
|
2110
|
-
* Turn admission and
|
|
2111
|
-
* unbounded control request stalls every session until the Host restarts.
|
|
2294
|
+
* Turn admission attempts and metadata reads share one process-wide gate,
|
|
2295
|
+
* so an unbounded control request stalls every session until the Host restarts.
|
|
2112
2296
|
* Bounding it is only half the cure: a timeout also proves this query has
|
|
2113
2297
|
* stopped answering, and keeping the entry means the next caller reuses the
|
|
2114
2298
|
* same dead process — timing out again, forever. Discarding it lets the next
|
|
@@ -2129,32 +2313,97 @@ var ClaudeSupervisor = class {
|
|
|
2129
2313
|
await this.#control(entry, entry.query.setPermissionMode(mode), "Claude Code permission mode switch");
|
|
2130
2314
|
entry.permissionMode = mode;
|
|
2131
2315
|
}
|
|
2316
|
+
#finishMetadataAdmission(admission) {
|
|
2317
|
+
this.#metadataAdmissions.delete(admission);
|
|
2318
|
+
admission.complete();
|
|
2319
|
+
}
|
|
2320
|
+
#cancelMetadataAdmissions(predicate) {
|
|
2321
|
+
const cancelled = [...this.#metadataAdmissions].filter(predicate);
|
|
2322
|
+
for (const admission of cancelled) {
|
|
2323
|
+
admission.cancellation.abort();
|
|
2324
|
+
if (!admission.started) this.#finishMetadataAdmission(admission);
|
|
2325
|
+
}
|
|
2326
|
+
return cancelled.map((admission) => admission.completion);
|
|
2327
|
+
}
|
|
2328
|
+
#cancelTurnAdmissions(predicate, error) {
|
|
2329
|
+
const cancelled = this.#turnAdmissions.filter(predicate);
|
|
2330
|
+
for (const admission of cancelled) {
|
|
2331
|
+
admission.cancellation.abort();
|
|
2332
|
+
if (!admission.delivered) {
|
|
2333
|
+
admission.delivered = true;
|
|
2334
|
+
admission.reject(error);
|
|
2335
|
+
}
|
|
2336
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error });
|
|
2337
|
+
}
|
|
2338
|
+
if (cancelled.length > 0) {
|
|
2339
|
+
this.#admissionRevision += 1;
|
|
2340
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2341
|
+
this.#scheduleTurnAdmissions();
|
|
2342
|
+
}
|
|
2343
|
+
return cancelled.map((admission) => admission.completion);
|
|
2344
|
+
}
|
|
2345
|
+
limitsChanged() {
|
|
2346
|
+
if (this.#disposed) return;
|
|
2347
|
+
this.#notifyCapacityChange();
|
|
2348
|
+
this.#scheduleLimitReconciliation();
|
|
2349
|
+
}
|
|
2132
2350
|
async disposeSession(sessionId) {
|
|
2351
|
+
const pendingAdmissions = this.#cancelTurnAdmissions((admission) => admission.request.agent.id === sessionId, abortFailure());
|
|
2352
|
+
const pendingMetadata = this.#cancelMetadataAdmissions((admission) => admission.sessionId === sessionId);
|
|
2133
2353
|
const entry = this.#entries.get(sessionId);
|
|
2134
|
-
if (entry
|
|
2135
|
-
|
|
2136
|
-
|
|
2354
|
+
if (entry !== void 0) this.#entries.delete(sessionId);
|
|
2355
|
+
await Promise.allSettled([
|
|
2356
|
+
...pendingAdmissions,
|
|
2357
|
+
...pendingMetadata,
|
|
2358
|
+
...entry === void 0 ? [] : [this.#disposeEntry(entry)]
|
|
2359
|
+
]);
|
|
2137
2360
|
}
|
|
2138
2361
|
async dispose() {
|
|
2139
2362
|
if (this.#disposed) return;
|
|
2140
2363
|
this.#disposed = true;
|
|
2364
|
+
const pendingAdmissions = this.#cancelTurnAdmissions(() => true, /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed"));
|
|
2365
|
+
const pendingMetadata = this.#cancelMetadataAdmissions(() => true);
|
|
2141
2366
|
const entries = [...this.#entries.values()];
|
|
2142
2367
|
this.#entries.clear();
|
|
2143
|
-
|
|
2368
|
+
this.#notifyCapacityChange();
|
|
2369
|
+
await Promise.allSettled([
|
|
2370
|
+
...pendingAdmissions,
|
|
2371
|
+
...pendingMetadata,
|
|
2372
|
+
...entries.map((entry) => this.#disposeEntry(entry))
|
|
2373
|
+
]);
|
|
2144
2374
|
}
|
|
2145
2375
|
async #makeRoom() {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2376
|
+
while (this.#entries.size >= this.#config.maxProcesses) {
|
|
2377
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2378
|
+
if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2379
|
+
this.#entries.delete(idle.sessionId);
|
|
2380
|
+
await this.#disposeEntry(idle);
|
|
2381
|
+
}
|
|
2151
2382
|
}
|
|
2152
|
-
async #
|
|
2383
|
+
async #trimExcessIdle() {
|
|
2384
|
+
while (this.#entries.size > this.#config.maxProcesses) {
|
|
2385
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2386
|
+
if (idle === void 0) return;
|
|
2387
|
+
this.#entries.delete(idle.sessionId);
|
|
2388
|
+
await this.#disposeEntry(idle);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
#notifyCapacityChange() {
|
|
2392
|
+
this.#admissionRevision += 1;
|
|
2393
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2394
|
+
this.#scheduleTurnAdmissions();
|
|
2395
|
+
}
|
|
2396
|
+
#scheduleLimitReconciliation() {
|
|
2397
|
+
const operation = this.#admissionGate.then(() => this.#trimExcessIdle());
|
|
2398
|
+
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
2399
|
+
}
|
|
2400
|
+
async #createEntry(agent, model, thinkingMode, signal, cancellationSignal) {
|
|
2153
2401
|
const sessionId = agent.id;
|
|
2154
2402
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
2155
2403
|
const input = new AsyncQueue();
|
|
2156
2404
|
const lifetime = new AbortController();
|
|
2157
2405
|
const projection = await this.#sidecar.importLegacy(sessionId, agent.session.events);
|
|
2406
|
+
if (signalAborted(signal) || signalAborted(cancellationSignal)) throw abortFailure();
|
|
2158
2407
|
const binding = projection.binding;
|
|
2159
2408
|
const pendingRewind = projection.rewind?.pending;
|
|
2160
2409
|
const forkAt = pendingRewind !== void 0 && "resumeAt" in pendingRewind ? pendingRewind.resumeAt : void 0;
|
|
@@ -2677,6 +2926,8 @@ var ClaudeSupervisor = class {
|
|
|
2677
2926
|
await this.#recordChainAnchor(entry, active);
|
|
2678
2927
|
this.#checkpointProjection(entry);
|
|
2679
2928
|
this.#armIdleTimer(entry);
|
|
2929
|
+
this.#notifyCapacityChange();
|
|
2930
|
+
this.#scheduleLimitReconciliation();
|
|
2680
2931
|
return;
|
|
2681
2932
|
}
|
|
2682
2933
|
if (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0) {
|
|
@@ -2758,9 +3009,11 @@ var ClaudeSupervisor = class {
|
|
|
2758
3009
|
entry.state = "idle";
|
|
2759
3010
|
entry.lastUsedAt = Date.now();
|
|
2760
3011
|
await this.#recordChainAnchor(entry, active);
|
|
2761
|
-
await this.#learnContextWindow(entry);
|
|
2762
3012
|
this.#checkpointProjection(entry);
|
|
2763
3013
|
this.#armIdleTimer(entry);
|
|
3014
|
+
this.#notifyCapacityChange();
|
|
3015
|
+
this.#scheduleLimitReconciliation();
|
|
3016
|
+
await this.#learnContextWindow(entry);
|
|
2764
3017
|
}
|
|
2765
3018
|
/** Tell every reader where this session's delta stream ended.
|
|
2766
3019
|
*
|
|
@@ -2942,6 +3195,7 @@ var ClaudeSupervisor = class {
|
|
|
2942
3195
|
if (entry.process !== void 0) try {
|
|
2943
3196
|
await entry.process.handle.waitForExit(AbortSignal.timeout(5e3));
|
|
2944
3197
|
} catch {}
|
|
3198
|
+
this.#notifyCapacityChange();
|
|
2945
3199
|
}
|
|
2946
3200
|
};
|
|
2947
3201
|
//#endregion
|
|
@@ -3995,7 +4249,11 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3995
4249
|
}
|
|
3996
4250
|
}));
|
|
3997
4251
|
for (const sessionId of sessionIds) writeSnapshot(sessionId).catch(() => void 0);
|
|
4252
|
+
let sweeping = false;
|
|
3998
4253
|
const timer = setInterval(() => {
|
|
4254
|
+
writeLine({ type: "ping" });
|
|
4255
|
+
if (sweeping) return;
|
|
4256
|
+
sweeping = true;
|
|
3999
4257
|
(async () => {
|
|
4000
4258
|
for (const sessionId of sessionIds) {
|
|
4001
4259
|
if (closed) return;
|
|
@@ -4004,8 +4262,9 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
4004
4262
|
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
4005
4263
|
writeMeta(sessionId, next);
|
|
4006
4264
|
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4265
|
+
})().catch(() => void 0).finally(() => {
|
|
4266
|
+
sweeping = false;
|
|
4267
|
+
});
|
|
4009
4268
|
}, META_REFRESH_MS);
|
|
4010
4269
|
timer.unref?.();
|
|
4011
4270
|
await new Promise((resolve) => {
|
|
@@ -6062,6 +6321,358 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
|
6062
6321
|
});
|
|
6063
6322
|
}
|
|
6064
6323
|
//#endregion
|
|
6324
|
+
//#region src/prompts.ts
|
|
6325
|
+
const MAX_PROMPT_FILES = 256;
|
|
6326
|
+
const MAX_PROMPT_BYTES = 16384;
|
|
6327
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
6328
|
+
const MAX_REQUEST_BYTES$1 = 32768;
|
|
6329
|
+
/**
|
|
6330
|
+
* The same shape as the output-style name in `global-settings.ts`, plus
|
|
6331
|
+
* `\p{M}` so a macOS-decomposed name survives. Two properties matter and both
|
|
6332
|
+
* are load-bearing: no member of the class is a path separator, and the first
|
|
6333
|
+
* character can be neither a dot nor a separator — so `join(dir, name + '.md')`
|
|
6334
|
+
* cannot address anything outside `dir`. Nothing else validates the path.
|
|
6335
|
+
*/
|
|
6336
|
+
const PROMPT_NAME = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._()\[\]-]{0,127}$/u;
|
|
6337
|
+
/** Prompt snippets live beside the rest of the user's Claude Code state. */
|
|
6338
|
+
function claudePromptsDir() {
|
|
6339
|
+
return join(homedir(), ".claude", "prompts");
|
|
6340
|
+
}
|
|
6341
|
+
/** `~/.claude/prompts/x.md` rather than the absolute path it expands to. */
|
|
6342
|
+
function displayPath(file) {
|
|
6343
|
+
const home = homedir();
|
|
6344
|
+
return file.startsWith(`${home}/`) ? `~${file.slice(home.length)}` : file;
|
|
6345
|
+
}
|
|
6346
|
+
/** The menu's second row: the first non-empty line, collapsed and bounded. */
|
|
6347
|
+
function summarize(body) {
|
|
6348
|
+
const line = body.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "";
|
|
6349
|
+
return redactText(line.replace(/\s+/gu, " "), MAX_DESCRIPTION_CHARS);
|
|
6350
|
+
}
|
|
6351
|
+
/**
|
|
6352
|
+
* Every `.md` file in the prompts directory, sorted by name.
|
|
6353
|
+
*
|
|
6354
|
+
* A missing directory is the ordinary state before the user saves anything,
|
|
6355
|
+
* and one unreadable file must not empty the whole menu — both answer with
|
|
6356
|
+
* what is there rather than throwing.
|
|
6357
|
+
*/
|
|
6358
|
+
async function readClaudePrompts(directory) {
|
|
6359
|
+
const prompts = [];
|
|
6360
|
+
let entries;
|
|
6361
|
+
try {
|
|
6362
|
+
entries = await opendir(directory);
|
|
6363
|
+
} catch (error) {
|
|
6364
|
+
if (error.code === "ENOENT") return prompts;
|
|
6365
|
+
throw error;
|
|
6366
|
+
}
|
|
6367
|
+
for await (const entry of entries) {
|
|
6368
|
+
if (prompts.length >= MAX_PROMPT_FILES) break;
|
|
6369
|
+
if (!entry.isFile() || extname(entry.name).toLowerCase() !== ".md") continue;
|
|
6370
|
+
const name = entry.name.slice(0, -3);
|
|
6371
|
+
if (!PROMPT_NAME.test(name)) continue;
|
|
6372
|
+
try {
|
|
6373
|
+
const file = join(directory, entry.name);
|
|
6374
|
+
const body = await readFile(file, "utf8");
|
|
6375
|
+
if (body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) continue;
|
|
6376
|
+
prompts.push({
|
|
6377
|
+
name,
|
|
6378
|
+
description: summarize(body),
|
|
6379
|
+
body,
|
|
6380
|
+
location: displayPath(file)
|
|
6381
|
+
});
|
|
6382
|
+
} catch {}
|
|
6383
|
+
}
|
|
6384
|
+
return prompts.sort((left, right) => left.name.localeCompare(right.name));
|
|
6385
|
+
}
|
|
6386
|
+
var ClaudePromptWriteError = class extends Error {
|
|
6387
|
+
code;
|
|
6388
|
+
constructor(code, message) {
|
|
6389
|
+
super(message);
|
|
6390
|
+
this.name = "ClaudePromptWriteError";
|
|
6391
|
+
this.code = code;
|
|
6392
|
+
}
|
|
6393
|
+
};
|
|
6394
|
+
/**
|
|
6395
|
+
* Save one prompt, refusing to clobber an existing file.
|
|
6396
|
+
*
|
|
6397
|
+
* `wx` is the whole collision policy: a name the user already uses is theirs
|
|
6398
|
+
* to resolve, and overwriting a prompt they keep is not something they can
|
|
6399
|
+
* undo from inside DSH.
|
|
6400
|
+
*/
|
|
6401
|
+
async function writeClaudePrompt(directory, name, body) {
|
|
6402
|
+
if (typeof name !== "string" || !PROMPT_NAME.test(name)) throw new ClaudePromptWriteError("invalid-name", "The prompt name is invalid.");
|
|
6403
|
+
if (typeof body !== "string" || body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) throw new ClaudePromptWriteError("invalid-body", "The prompt text is empty or too large.");
|
|
6404
|
+
const text = body.endsWith("\n") ? body : `${body}\n`;
|
|
6405
|
+
const file = join(directory, `${name}.md`);
|
|
6406
|
+
await mkdir(directory, {
|
|
6407
|
+
recursive: true,
|
|
6408
|
+
mode: 448
|
|
6409
|
+
});
|
|
6410
|
+
try {
|
|
6411
|
+
await writeFile(file, text, {
|
|
6412
|
+
encoding: "utf8",
|
|
6413
|
+
mode: 384,
|
|
6414
|
+
flag: "wx"
|
|
6415
|
+
});
|
|
6416
|
+
} catch (error) {
|
|
6417
|
+
if (error.code === "EEXIST") throw new ClaudePromptWriteError("name-taken", "A prompt with that name already exists.");
|
|
6418
|
+
throw error;
|
|
6419
|
+
}
|
|
6420
|
+
return {
|
|
6421
|
+
name,
|
|
6422
|
+
description: summarize(text),
|
|
6423
|
+
body: text,
|
|
6424
|
+
location: displayPath(file)
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
const MAX_NAME_DRAFT_CHARS = 4e3;
|
|
6428
|
+
const MAX_NAME_OUTPUT_BYTES = 4096;
|
|
6429
|
+
const MAX_SUGGESTED_NAME_CHARS = 48;
|
|
6430
|
+
const MAX_REFINE_DRAFT_CHARS = 16e3;
|
|
6431
|
+
const MAX_REFINED_BYTES = 32768;
|
|
6432
|
+
const ASSIST_TIMEOUT_MS = 4e4;
|
|
6433
|
+
/**
|
|
6434
|
+
* Ask for a file name and nothing else, with the draft fenced as data.
|
|
6435
|
+
*
|
|
6436
|
+
* Two things here were measured rather than guessed. The instruction travels
|
|
6437
|
+
* in the user turn, not in `--system-prompt`: a saved prompt is itself an
|
|
6438
|
+
* instruction and a system prompt does not outrank it, so the model reads the
|
|
6439
|
+
* draft as its task and answers it instead of naming it. And the default
|
|
6440
|
+
* system prompt is left in place even though it is large, because the CLI
|
|
6441
|
+
* sends it as a cache read (~6.5k cached tokens); replacing it with a short
|
|
6442
|
+
* one costs a fresh 3.5k-token prefill and measured slower end to end.
|
|
6443
|
+
*
|
|
6444
|
+
* The naming spec is this specific because a vaguer one ("describe what the
|
|
6445
|
+
* template does, at most 40 characters") produced names that were both
|
|
6446
|
+
* inconsistent in style and stripped of the detail that tells two similar
|
|
6447
|
+
* templates apart.
|
|
6448
|
+
*/
|
|
6449
|
+
function promptNamePrompt(draft) {
|
|
6450
|
+
return [
|
|
6451
|
+
"Below is a reusable prompt template a user is saving to a file. Name it.",
|
|
6452
|
+
"",
|
|
6453
|
+
`"""\n${draft.trim().slice(0, MAX_NAME_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6454
|
+
"",
|
|
6455
|
+
"Answer with the file name alone: no quotes, no explanation, no extension, no leading dot, no slashes.",
|
|
6456
|
+
"Write it in English however the template is written, as lower-case words joined by hyphens.",
|
|
6457
|
+
"Name the task the template performs, keeping whatever detail distinguishes it from a",
|
|
6458
|
+
"similar template. At most 6 words."
|
|
6459
|
+
].join("\n");
|
|
6460
|
+
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Rewrite the draft into something an agent can act on, with the draft fenced
|
|
6463
|
+
* as data for the same reason the naming prompt fences it.
|
|
6464
|
+
*
|
|
6465
|
+
* The rule about people and places is not politeness. Asked to rewrite
|
|
6466
|
+
* "后端 assign 给我", the model reached into Claude Code's ambient system
|
|
6467
|
+
* prompt and substituted the operator's actual email address — inventing a
|
|
6468
|
+
* detail the original never carried, and putting a personal address into a
|
|
6469
|
+
* message the user had not written it into. Naming the failure explicitly is
|
|
6470
|
+
* what stopped it; stripping the ambient context with `--system-prompt` also
|
|
6471
|
+
* stopped it but cost a fresh prefill and produced looser rewrites.
|
|
6472
|
+
*/
|
|
6473
|
+
function promptRefinePrompt(draft) {
|
|
6474
|
+
return [
|
|
6475
|
+
"Below is a prompt a user is about to send to a coding agent. Rewrite it so the agent can act on it without coming back with questions.",
|
|
6476
|
+
"",
|
|
6477
|
+
`"""\n${draft.trim().slice(0, MAX_REFINE_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6478
|
+
"",
|
|
6479
|
+
"Keep the intent and every concrete detail exactly as given — names, paths, branches, identifiers, numbers.",
|
|
6480
|
+
"Leave every reference to a person or place as the original wrote it (\"me\", \"我\", \"the usual branch\");",
|
|
6481
|
+
"you do not know who or what they resolve to, and guessing puts a wrong name in the user's message.",
|
|
6482
|
+
"Do not invent requirements the original does not imply, do not answer the prompt, do not add a preamble or sign-off.",
|
|
6483
|
+
"Write the rewrite in the same language the original is written in.",
|
|
6484
|
+
"Reply with the rewritten prompt alone."
|
|
6485
|
+
].join("\n");
|
|
6486
|
+
}
|
|
6487
|
+
/** One turn, no tools, no MCP: neither of these tasks needs them, and both
|
|
6488
|
+
* cost seconds of cold start. Variadic `--tools` stays last. */
|
|
6489
|
+
function promptAssistArguments() {
|
|
6490
|
+
return [
|
|
6491
|
+
"-p",
|
|
6492
|
+
"--output-format",
|
|
6493
|
+
"text",
|
|
6494
|
+
"--model",
|
|
6495
|
+
"haiku",
|
|
6496
|
+
"--strict-mcp-config",
|
|
6497
|
+
"--mcp-config",
|
|
6498
|
+
"{\"mcpServers\":{}}",
|
|
6499
|
+
"--tools",
|
|
6500
|
+
""
|
|
6501
|
+
];
|
|
6502
|
+
}
|
|
6503
|
+
/** The rewrite in a model reply, or undefined when the reply is unusable.
|
|
6504
|
+
*
|
|
6505
|
+
* A model told to answer with the text alone still sometimes wraps it in a
|
|
6506
|
+
* markdown fence, so one is peeled off; anything else is the user's to read
|
|
6507
|
+
* and edit, and is passed through untouched. */
|
|
6508
|
+
function refinedPrompt(output) {
|
|
6509
|
+
const trimmed = output.trim();
|
|
6510
|
+
const text = (/^```[^\n]*\n([\s\S]*?)\n?```$/u.exec(trimmed)?.[1] ?? trimmed).trim();
|
|
6511
|
+
return text.length === 0 || Buffer.byteLength(text) > MAX_REFINED_BYTES ? void 0 : text;
|
|
6512
|
+
}
|
|
6513
|
+
/**
|
|
6514
|
+
* The usable name in a model reply, or undefined when there is none.
|
|
6515
|
+
*
|
|
6516
|
+
* A one-line answer is what the prompt asks for and usually what comes back,
|
|
6517
|
+
* but "usually" is not a contract: the reply is scrubbed to what a file name
|
|
6518
|
+
* may hold and then held to the same {@link PROMPT_NAME} guard the write path
|
|
6519
|
+
* uses, so a chatty or malformed answer is dropped rather than offered.
|
|
6520
|
+
*/
|
|
6521
|
+
function suggestedName(output) {
|
|
6522
|
+
const scrubbed = wordBounded((output.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "").replace(/^["'`]+|["'`]+$/gu, "").replace(/\.md$/iu, "").replace(/[^\p{L}\p{M}\p{N} ._()\[\]-]/gu, " ").replace(/\s+/gu, " ").trim());
|
|
6523
|
+
return PROMPT_NAME.test(scrubbed) ? scrubbed : void 0;
|
|
6524
|
+
}
|
|
6525
|
+
/** Cut a long name at its last word boundary. A name the user has to repair
|
|
6526
|
+
* ("analyze-frontend-backend-create-jira-tic") is worse than a shorter one. */
|
|
6527
|
+
function wordBounded(name) {
|
|
6528
|
+
if (name.length <= MAX_SUGGESTED_NAME_CHARS) return name;
|
|
6529
|
+
const cut = name.slice(0, MAX_SUGGESTED_NAME_CHARS);
|
|
6530
|
+
const boundary = cut.search(/[\s\-_][^\s\-_]*$/u);
|
|
6531
|
+
return (boundary > 0 ? cut.slice(0, boundary) : cut).trim();
|
|
6532
|
+
}
|
|
6533
|
+
/** Runs Claude Code's cheapest model over the draft: names it, or rewrites it. */
|
|
6534
|
+
var PromptAssistService = class {
|
|
6535
|
+
#runtime;
|
|
6536
|
+
#executablePath;
|
|
6537
|
+
constructor(runtime, executablePath) {
|
|
6538
|
+
this.#runtime = runtime;
|
|
6539
|
+
this.#executablePath = executablePath;
|
|
6540
|
+
}
|
|
6541
|
+
/** One bounded, tool-less turn; undefined whenever it cannot be had. */
|
|
6542
|
+
async #ask(prompt, maxOutputBytes, signal) {
|
|
6543
|
+
const executablePath = this.#executablePath();
|
|
6544
|
+
if (executablePath.length === 0) return void 0;
|
|
6545
|
+
const timeout = AbortSignal.timeout(ASSIST_TIMEOUT_MS);
|
|
6546
|
+
try {
|
|
6547
|
+
const handle = this.#runtime.spawn({
|
|
6548
|
+
argv: [executablePath, ...promptAssistArguments()],
|
|
6549
|
+
cwd: homedir(),
|
|
6550
|
+
stdio: {
|
|
6551
|
+
stdin: { data: prompt },
|
|
6552
|
+
stdout: { maxBytes: maxOutputBytes },
|
|
6553
|
+
stderr: { maxBytes: MAX_NAME_OUTPUT_BYTES }
|
|
6554
|
+
},
|
|
6555
|
+
graceMs: 1e3,
|
|
6556
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
|
|
6557
|
+
env: { MAX_THINKING_TOKENS: "0" }
|
|
6558
|
+
});
|
|
6559
|
+
if ((await handle.done).exitCode !== 0) return void 0;
|
|
6560
|
+
return handle.collected.stdout?.readFrom(0).text ?? "";
|
|
6561
|
+
} catch {
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
/** The suggested file name, or undefined whenever one cannot be had. The
|
|
6566
|
+
* caller already has a name derived locally, so every failure here is a
|
|
6567
|
+
* non-event: nothing is reported, and nothing is retried. */
|
|
6568
|
+
async suggest(draft, signal) {
|
|
6569
|
+
if (draft.trim().length === 0) return void 0;
|
|
6570
|
+
const output = await this.#ask(promptNamePrompt(draft), MAX_NAME_OUTPUT_BYTES, signal);
|
|
6571
|
+
return output === void 0 ? void 0 : suggestedName(output);
|
|
6572
|
+
}
|
|
6573
|
+
/** The rewritten draft, or undefined when the rewrite could not be had.
|
|
6574
|
+
* Unlike naming, this failure is worth reporting: the user asked for it and
|
|
6575
|
+
* is waiting on it. */
|
|
6576
|
+
async refine(draft, signal) {
|
|
6577
|
+
if (draft.trim().length === 0) return void 0;
|
|
6578
|
+
const output = await this.#ask(promptRefinePrompt(draft), MAX_REFINED_BYTES, signal);
|
|
6579
|
+
return output === void 0 ? void 0 : refinedPrompt(output);
|
|
6580
|
+
}
|
|
6581
|
+
};
|
|
6582
|
+
/** List the user's prompt snippets, and save the composer draft as a new one. */
|
|
6583
|
+
function registerClaudePromptsRoute(ctx, directory = claudePromptsDir()) {
|
|
6584
|
+
registerPluginRoute(ctx, {
|
|
6585
|
+
mode: "unary",
|
|
6586
|
+
kind: "exact",
|
|
6587
|
+
path: CLAUDE_PROMPTS_PATH,
|
|
6588
|
+
methods: ["GET", "POST"],
|
|
6589
|
+
budget: "fast",
|
|
6590
|
+
handler: async (io) => {
|
|
6591
|
+
if (io.method === "GET") return {
|
|
6592
|
+
status: 200,
|
|
6593
|
+
value: { prompts: await readClaudePrompts(directory) }
|
|
6594
|
+
};
|
|
6595
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6596
|
+
try {
|
|
6597
|
+
return {
|
|
6598
|
+
status: 200,
|
|
6599
|
+
value: {
|
|
6600
|
+
saved: true,
|
|
6601
|
+
prompt: await writeClaudePrompt(directory, payload.name, payload.body)
|
|
6602
|
+
}
|
|
6603
|
+
};
|
|
6604
|
+
} catch (error) {
|
|
6605
|
+
if (error instanceof ClaudePromptWriteError) return {
|
|
6606
|
+
status: error.code === "name-taken" ? 409 : 400,
|
|
6607
|
+
value: {
|
|
6608
|
+
error: error.code,
|
|
6609
|
+
message: error.message
|
|
6610
|
+
}
|
|
6611
|
+
};
|
|
6612
|
+
return {
|
|
6613
|
+
status: 500,
|
|
6614
|
+
value: {
|
|
6615
|
+
error: "prompt-write-failed",
|
|
6616
|
+
message: "The prompt could not be saved."
|
|
6617
|
+
}
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
6622
|
+
}
|
|
6623
|
+
/** Suggest a file name for a draft. Answers `{}` when no name could be had:
|
|
6624
|
+
* the caller keeps the name it derived locally, so this is never an error. */
|
|
6625
|
+
function registerClaudePromptNameRoute(ctx, service) {
|
|
6626
|
+
registerPluginRoute(ctx, {
|
|
6627
|
+
mode: "unary",
|
|
6628
|
+
kind: "exact",
|
|
6629
|
+
path: CLAUDE_PROMPT_NAME_PATH,
|
|
6630
|
+
methods: ["POST"],
|
|
6631
|
+
budget: "git",
|
|
6632
|
+
handler: async (io) => {
|
|
6633
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6634
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6635
|
+
const name = await service.suggest(draft, io.signal);
|
|
6636
|
+
return {
|
|
6637
|
+
status: 200,
|
|
6638
|
+
value: name === void 0 ? {} : { name }
|
|
6639
|
+
};
|
|
6640
|
+
}
|
|
6641
|
+
});
|
|
6642
|
+
}
|
|
6643
|
+
/** Rewrite a draft into something an agent can act on. */
|
|
6644
|
+
function registerClaudePromptRefineRoute(ctx, service) {
|
|
6645
|
+
registerPluginRoute(ctx, {
|
|
6646
|
+
mode: "unary",
|
|
6647
|
+
kind: "exact",
|
|
6648
|
+
path: CLAUDE_PROMPT_REFINE_PATH,
|
|
6649
|
+
methods: ["POST"],
|
|
6650
|
+
budget: "git",
|
|
6651
|
+
handler: async (io) => {
|
|
6652
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6653
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6654
|
+
if (draft.trim().length === 0) return {
|
|
6655
|
+
status: 400,
|
|
6656
|
+
value: {
|
|
6657
|
+
error: "empty-draft",
|
|
6658
|
+
message: "There is nothing to rewrite."
|
|
6659
|
+
}
|
|
6660
|
+
};
|
|
6661
|
+
const text = await service.refine(draft, io.signal);
|
|
6662
|
+
return text === void 0 ? {
|
|
6663
|
+
status: 503,
|
|
6664
|
+
value: {
|
|
6665
|
+
error: "refine-unavailable",
|
|
6666
|
+
message: "Claude could not rewrite the prompt."
|
|
6667
|
+
}
|
|
6668
|
+
} : {
|
|
6669
|
+
status: 200,
|
|
6670
|
+
value: { text }
|
|
6671
|
+
};
|
|
6672
|
+
}
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
//#endregion
|
|
6065
6676
|
//#region src/github-url.ts
|
|
6066
6677
|
/** Only GitHub's own image hosts; the browser loads these directly, so a URL
|
|
6067
6678
|
* the API did not vouch for must never become an outbound request. */
|
|
@@ -8158,12 +8769,12 @@ const ALERTS = {
|
|
|
8158
8769
|
function isBoundedInteger(value, min, max) {
|
|
8159
8770
|
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
8160
8771
|
}
|
|
8161
|
-
function integerSetting(key, min, max, defaultFor) {
|
|
8772
|
+
function integerSetting(key, min, max, defaultFor, effect = "new-session") {
|
|
8162
8773
|
return {
|
|
8163
8774
|
key,
|
|
8164
8775
|
kind: "text",
|
|
8165
8776
|
document: "plugin",
|
|
8166
|
-
effect
|
|
8777
|
+
effect,
|
|
8167
8778
|
maxLength: String(max).length,
|
|
8168
8779
|
read(document, defaults = DEFAULT_LIMITS) {
|
|
8169
8780
|
const value = document[key];
|
|
@@ -8182,7 +8793,7 @@ const DESCRIPTORS = [
|
|
|
8182
8793
|
PROSE,
|
|
8183
8794
|
ALERTS,
|
|
8184
8795
|
WORKTREE_BRANCH_PREFIX,
|
|
8185
|
-
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
8796
|
+
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses, "immediate"),
|
|
8186
8797
|
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
8187
8798
|
];
|
|
8188
8799
|
const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
|
|
@@ -8595,7 +9206,10 @@ async function apply(ctx, config) {
|
|
|
8595
9206
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
8596
9207
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
8597
9208
|
defaultLimits,
|
|
8598
|
-
onUpdated:
|
|
9209
|
+
onUpdated: async () => {
|
|
9210
|
+
await applySettingsOverrides();
|
|
9211
|
+
supervisor.limitsChanged();
|
|
9212
|
+
}
|
|
8599
9213
|
});
|
|
8600
9214
|
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
|
|
8601
9215
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
@@ -8609,6 +9223,10 @@ async function apply(ctx, config) {
|
|
|
8609
9223
|
};
|
|
8610
9224
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
8611
9225
|
registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
|
|
9226
|
+
registerClaudePromptsRoute(webCtx);
|
|
9227
|
+
const promptAssist = new PromptAssistService(webCtx.subprocess, () => supervisorConfig.executablePath);
|
|
9228
|
+
registerClaudePromptNameRoute(webCtx, promptAssist);
|
|
9229
|
+
registerClaudePromptRefineRoute(webCtx, promptAssist);
|
|
8612
9230
|
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
8613
9231
|
registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
8614
9232
|
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|