@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/lib/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { A as CLAUDE_REPOSITORY_STATUS_PATH, B as isClaudeAlertMode, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_REPOSITORY_FEEDBACK_PATH, E as CLAUDE_REPOSITORY_ACTION_PATH, F as CLAUDE_USAGE_PATH, H as isClaudeRenderMode, I as DEFAULT_CLAUDE_PROSE_MODE, L as DEFAULT_CLAUDE_RENDER_MODE, M as CLAUDE_REWIND_PATH, N as CLAUDE_UPDATE_CHECK_PATH, O as CLAUDE_REPOSITORY_FILE_PATH, P as CLAUDE_UPDATE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_RENDER_MODES, V 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_REVIEW_COMMENT_PATH, k as CLAUDE_REPOSITORY_SETUP_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_PROSE_MODES, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as TASK_TOOL_NAMES } from "./events-B-FPMzI7.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BV42EKkB.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-yRGfkGjd.mjs";
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
- const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
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
- return operation;
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
- entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
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
- await entry.sdkInitialization;
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 = await this.#createEntry(request.agent, model, request.thinkingMode);
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
- await entry.sdkInitialization;
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
- cursor.nextOrdinal = (await this.#sidecar.read(sessionId)).activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
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
- entry.state = "idle";
2056
- entry.lastUsedAt = Date.now();
2057
- this.#armIdleTimer(entry);
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
- const entry = await this.#metadataEntry(agent, model);
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
- entry = await this.#createEntry(agent, model);
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 every metadata read share one process-wide gate, so an
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 === void 0) return;
2135
- this.#entries.delete(sessionId);
2136
- await this.#disposeEntry(entry);
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
- await Promise.allSettled(entries.map((entry) => this.#disposeEntry(entry)));
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
- if (this.#entries.size < this.#config.maxProcesses) return;
2147
- const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
2148
- if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
2149
- this.#entries.delete(idle.sessionId);
2150
- await this.#disposeEntry(idle);
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 #createEntry(agent, model, thinkingMode) {
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
- writeLine({ type: "ping" });
4008
- })().catch(() => void 0);
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: "new-session",
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: applySettingsOverrides
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);