@lucascouts/claude-agent-acp-plus 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-agent.d.ts +99 -23
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +264 -85
- package/dist/ultracode.d.ts +138 -72
- package/dist/ultracode.d.ts.map +1 -1
- package/dist/ultracode.js +208 -75
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -19,7 +19,7 @@ import { filterSupersededModels } from "./model-recency.js";
|
|
|
19
19
|
import { SettingsManager } from "./settings.js";
|
|
20
20
|
import { ContextCompactionLifecycle, contextCompactionMetadataFromBoundary, } from "./context-compaction.js";
|
|
21
21
|
import { createThinkingConfigOption, effectiveThinkingConfig, resolveThinkingSelection, THINKING_CONFIG_ID, } from "./thinking-option.js";
|
|
22
|
-
import {
|
|
22
|
+
import { applyLegacyUltracodeEffort, applyUltracodeReadback, applyUltracodeToggle, effortFlagSettings, isLegacyUltracodeEffort, isUltracodeAvailable, readUltracodeState, resolveUltracodeRequested, ULTRACODE_CONFIG_ID, ultracodeConfigOption, } from "./ultracode.js";
|
|
23
23
|
import { handleRewindCommand, parseRewindInvocation } from "./rewind-command.js";
|
|
24
24
|
import { resolvePermissionMode, sessionAllowsBypass } from "./permissions/modes.js";
|
|
25
25
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
@@ -152,6 +152,28 @@ export const TASKS_OUTPUT_METHOD = "_claude/tasks/output";
|
|
|
152
152
|
export const TASKS_CAPABILITY_KEY = TASKS_META_KEY;
|
|
153
153
|
/** Version 2 (story 041) adds `hasOutput`, `tail` and `_claude/tasks/output`. */
|
|
154
154
|
export const TASKS_CAPABILITY_VERSION = TASKS_CONTRACT_VERSION;
|
|
155
|
+
/** Refresh the account quota windows of one session now, on demand. Params
|
|
156
|
+
* {@link UsageRefreshRequest}, result {@link UsageRefreshResponse}. The windows
|
|
157
|
+
* themselves arrive the way they always do — one `usage_update` per window,
|
|
158
|
+
* carrying `_meta["_claude/rateLimit"]` — before the response does. */
|
|
159
|
+
export const USAGE_REFRESH_METHOD = "_claude/usage/refresh";
|
|
160
|
+
/** `agentCapabilities._meta` key advertising {@link USAGE_REFRESH_METHOD}. A
|
|
161
|
+
* client gates on this, never on the agent's name (see
|
|
162
|
+
* {@link TASKS_CAPABILITY_KEY}). */
|
|
163
|
+
export const USAGE_CAPABILITY_KEY = "_claude/usage";
|
|
164
|
+
export const USAGE_CAPABILITY_VERSION = 1;
|
|
165
|
+
/** How old a shared quota sample may be and still answer an on-demand refresh.
|
|
166
|
+
*
|
|
167
|
+
* The client asks on every hover of the context ring, so this is the knob that
|
|
168
|
+
* trades freshness against cost. Five seconds is shorter than any change a
|
|
169
|
+
* person reading a tooltip could tell apart — neither the five-hour nor the
|
|
170
|
+
* weekly window moves measurably in that time — yet long enough that a burst
|
|
171
|
+
* of hovers, or several Zeo windows hovered in turn, costs one control request
|
|
172
|
+
* between them rather than one each. Much shorter than the idle poll's
|
|
173
|
+
* {@link sharedSampleMaxAgeMs}: a hover is someone asking "what is it NOW?",
|
|
174
|
+
* and a sample most of an interval old is exactly what the poll already
|
|
175
|
+
* showed them. */
|
|
176
|
+
export const USAGE_REFRESH_MAX_SAMPLE_AGE_MS = 5_000;
|
|
155
177
|
/** The SDK's own text for a rejected control request, passed through to the
|
|
156
178
|
* client as the internal error's message (R2.4). */
|
|
157
179
|
function errorMessage(error) {
|
|
@@ -195,6 +217,10 @@ export function parseTasksBackgroundRequest(params) {
|
|
|
195
217
|
toolCallId: requireIdField(params, TASKS_BACKGROUND_METHOD, "toolCallId"),
|
|
196
218
|
};
|
|
197
219
|
}
|
|
220
|
+
/** Validate raw JSON-RPC params into a {@link UsageRefreshRequest}. */
|
|
221
|
+
export function parseUsageRefreshRequest(params) {
|
|
222
|
+
return { sessionId: requireIdField(params, USAGE_REFRESH_METHOD, "sessionId") };
|
|
223
|
+
}
|
|
198
224
|
/** How often the account quota windows are refreshed while a session sits idle,
|
|
199
225
|
* in milliseconds; `0` disables polling entirely.
|
|
200
226
|
*
|
|
@@ -204,10 +230,17 @@ export function parseTasksBackgroundRequest(params) {
|
|
|
204
230
|
* the old utilization, and the bar is wrong in the one direction that matters
|
|
205
231
|
* (it under-reports how much is available) until the next turn ends.
|
|
206
232
|
*
|
|
233
|
+
* Thirty seconds by default (sixty until the quota tooltip): the tooltip over
|
|
234
|
+
* the context ring shows these windows as numbers, so a reading up to a minute
|
|
235
|
+
* old was visible as one. The cost does not double with it — every adapter
|
|
236
|
+
* process on the desktop shares one sample per interval (quota-cache.ts), so
|
|
237
|
+
* it is still about one control request per interval per account. Hovering
|
|
238
|
+
* the ring asks for a fresher one on demand (`_claude/usage/refresh`).
|
|
239
|
+
*
|
|
207
240
|
* The floor is not timidity: each tick costs one control request, and neither
|
|
208
241
|
* the five-hour nor the seven-day window can move fast enough for a tighter
|
|
209
242
|
* loop to show anything a 15-second one would miss. */
|
|
210
|
-
const QUOTA_POLL_DEFAULT_MS =
|
|
243
|
+
const QUOTA_POLL_DEFAULT_MS = 30_000;
|
|
211
244
|
const QUOTA_POLL_FLOOR_MS = 15_000;
|
|
212
245
|
/** Whether sessions ask the SDK for progress summaries on subagent tasks
|
|
213
246
|
* (`CLAUDE_ACP_PROGRESS_SUMMARIES`). Only `1` or `true` turn it on; anything
|
|
@@ -251,7 +284,11 @@ export function quotaPollIntervalMs(raw = process.env.CLAUDE_ACP_QUOTA_POLL_MS)
|
|
|
251
284
|
* (the result is skipped, its trailing idle absorbed as owed, so the
|
|
252
285
|
* #825 detector can't fire); misrouting an autonomous result into the
|
|
253
286
|
* user lane is the bounded misattribution class this set exists to
|
|
254
|
-
* reduce.
|
|
287
|
+
* reduce.
|
|
288
|
+
*
|
|
289
|
+
* Exception: a result from one of these origins that names a pending prompt
|
|
290
|
+
* in `user_message_uuids` consumed that folded prompt and takes the user
|
|
291
|
+
* lane (see the result handler). */
|
|
255
292
|
const AUTONOMOUS_RESULT_ORIGINS = new Set([
|
|
256
293
|
"task-notification",
|
|
257
294
|
"peer",
|
|
@@ -872,6 +909,7 @@ export class ClaudeAcpAgent {
|
|
|
872
909
|
promptQueueing: true,
|
|
873
910
|
},
|
|
874
911
|
[TASKS_CAPABILITY_KEY]: { version: TASKS_CAPABILITY_VERSION },
|
|
912
|
+
[USAGE_CAPABILITY_KEY]: { version: USAGE_CAPABILITY_VERSION },
|
|
875
913
|
},
|
|
876
914
|
promptCapabilities: {
|
|
877
915
|
image: true,
|
|
@@ -1350,14 +1388,19 @@ export class ClaudeAcpAgent {
|
|
|
1350
1388
|
*/
|
|
1351
1389
|
/** Fetch the account's quota windows and publish them to this session.
|
|
1352
1390
|
*
|
|
1353
|
-
* `
|
|
1391
|
+
* `cacheMaxAgeMs` is the difference between the callers. A turn ending and a
|
|
1354
1392
|
* session being established are moments when THIS session's own consumption
|
|
1355
|
-
* just changed, so they always fetch. The idle poll
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
|
|
1393
|
+
* just changed, so they pass none and always fetch. The idle poll and the
|
|
1394
|
+
* on-demand refresh have no such reason: they are asking about an account,
|
|
1395
|
+
* and every other adapter process on the desktop is asking the same question
|
|
1396
|
+
* about the same account. They accept a shared sample up to that age instead
|
|
1397
|
+
* -- see quota-cache.ts for why sharing, not tighter intervals, is what makes
|
|
1398
|
+
* separate windows agree.
|
|
1399
|
+
*
|
|
1400
|
+
* Resolves `true` when at least one window was published, `false` when none
|
|
1401
|
+
* was: no report, a report with no plan windows (an API-key session), or a
|
|
1402
|
+
* session replaced while the request was in flight. Never rejects. */
|
|
1403
|
+
async publishAccountUsage(sessionId, session, { cacheMaxAgeMs } = {}) {
|
|
1361
1404
|
try {
|
|
1362
1405
|
// Not `getUsage`: this awful spelling is the only name the real `Query`
|
|
1363
1406
|
// answers to, and it is written out here rather than hidden behind a
|
|
@@ -1371,9 +1414,7 @@ export class ClaudeAcpAgent {
|
|
|
1371
1414
|
// A sample another process already paid for, when this call is allowed to
|
|
1372
1415
|
// take one. `null` covers every failure the cache can have, so a broken
|
|
1373
1416
|
// cache degrades to exactly the behaviour that existed before it.
|
|
1374
|
-
const cached =
|
|
1375
|
-
? await readFreshLimits(sharedSampleMaxAgeMs(quotaPollIntervalMs()))
|
|
1376
|
-
: null;
|
|
1417
|
+
const cached = cacheMaxAgeMs !== undefined ? await readFreshLimits(cacheMaxAgeMs) : null;
|
|
1377
1418
|
const report = cached ??
|
|
1378
1419
|
(await session.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({
|
|
1379
1420
|
skipBehaviors: true,
|
|
@@ -1388,7 +1429,7 @@ export class ClaudeAcpAgent {
|
|
|
1388
1429
|
// flight (provider switch, a changed cwd on session/load). A successor
|
|
1389
1430
|
// must not inherit its predecessor's windows.
|
|
1390
1431
|
if (this.sessions[sessionId] !== session) {
|
|
1391
|
-
return;
|
|
1432
|
+
return false;
|
|
1392
1433
|
}
|
|
1393
1434
|
// `usage_update` is the only carrier ACP gives this `_meta`, and it
|
|
1394
1435
|
// requires both numbers. `contextUsedTokens` is the session-scoped mirror
|
|
@@ -1404,6 +1445,7 @@ export class ClaudeAcpAgent {
|
|
|
1404
1445
|
// single report's bars disagree about the context they were measured in.
|
|
1405
1446
|
const used = session.contextUsedTokens ?? 0;
|
|
1406
1447
|
const size = session.contextWindowSize;
|
|
1448
|
+
let published = false;
|
|
1407
1449
|
for (const window of this.accountUsageTracker(session).windowsFrom(report)) {
|
|
1408
1450
|
await this.client.sessionUpdate({
|
|
1409
1451
|
sessionId,
|
|
@@ -1414,10 +1456,13 @@ export class ClaudeAcpAgent {
|
|
|
1414
1456
|
_meta: { "_claude/rateLimit": window },
|
|
1415
1457
|
},
|
|
1416
1458
|
});
|
|
1459
|
+
published = true;
|
|
1417
1460
|
}
|
|
1461
|
+
return published;
|
|
1418
1462
|
}
|
|
1419
1463
|
catch (error) {
|
|
1420
1464
|
this.logger.error(`Session ${sessionId}: structured usage report unavailable: ${error}`);
|
|
1465
|
+
return false;
|
|
1421
1466
|
}
|
|
1422
1467
|
}
|
|
1423
1468
|
/** Start the idle refresh of the account quota windows for this session.
|
|
@@ -1438,6 +1483,7 @@ export class ClaudeAcpAgent {
|
|
|
1438
1483
|
if (intervalMs === 0) {
|
|
1439
1484
|
return;
|
|
1440
1485
|
}
|
|
1486
|
+
const cacheMaxAgeMs = sharedSampleMaxAgeMs(intervalMs);
|
|
1441
1487
|
// Closure-local rather than a Session field: it guards this timer alone, and
|
|
1442
1488
|
// a slow report must not let ticks pile up into a queue of duplicate control
|
|
1443
1489
|
// requests against a session that is already struggling to answer one.
|
|
@@ -1456,7 +1502,7 @@ export class ClaudeAcpAgent {
|
|
|
1456
1502
|
inFlight = true;
|
|
1457
1503
|
// `publishAccountUsage` never rejects — it logs and returns — so this
|
|
1458
1504
|
// cannot become an unhandled rejection, and `finally` always re-opens.
|
|
1459
|
-
void this.publishAccountUsage(sessionId, session, {
|
|
1505
|
+
void this.publishAccountUsage(sessionId, session, { cacheMaxAgeMs }).finally(() => {
|
|
1460
1506
|
inFlight = false;
|
|
1461
1507
|
});
|
|
1462
1508
|
}, intervalMs);
|
|
@@ -1464,6 +1510,70 @@ export class ClaudeAcpAgent {
|
|
|
1464
1510
|
timer.unref?.();
|
|
1465
1511
|
session.accountUsageTimer = timer;
|
|
1466
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* `_claude/usage/refresh`: publish this session's quota windows now, on
|
|
1515
|
+
* demand, and answer whether any were published.
|
|
1516
|
+
*
|
|
1517
|
+
* The client asks on every hover of the context ring, so the cost is bounded
|
|
1518
|
+
* twice. A shared sample younger than {@link USAGE_REFRESH_MAX_SAMPLE_AGE_MS}
|
|
1519
|
+
* answers without a control request, and a refresh already in flight for this
|
|
1520
|
+
* session is joined rather than repeated — however fast the hovers come, one
|
|
1521
|
+
* session never has two of these requests queued.
|
|
1522
|
+
*
|
|
1523
|
+
* Skipped while a turn is in flight, answering `{ refreshed: false }`. The
|
|
1524
|
+
* report is read-only, but SDK control requests are SERIALIZED: an interrupt
|
|
1525
|
+
* queued behind a usage request waits for it (upstream #1216 measured
|
|
1526
|
+
* 0.7-1.1 s for the same queueing behind `getContextUsage`), and `cancel`
|
|
1527
|
+
* awaits its interrupt. A hover during a turn is the common case — people
|
|
1528
|
+
* watch the ring fill — so allowing it would put a round-trip in front of the
|
|
1529
|
+
* Stop button exactly when it is most likely to be pressed. Nothing is lost:
|
|
1530
|
+
* the turn's own end refreshes the windows (R1.2), and a live
|
|
1531
|
+
* `rate_limit_event` still lands mid-turn.
|
|
1532
|
+
*
|
|
1533
|
+
* Before the first turn a control request may not be serviced at all on older
|
|
1534
|
+
* CLIs (#886/#880); such a call simply waits, joined by every later hover,
|
|
1535
|
+
* alongside the establishment refresh that is already waiting the same way.
|
|
1536
|
+
*
|
|
1537
|
+
* Never rejects for a missing report: a fetch failure is logged by
|
|
1538
|
+
* `publishAccountUsage` and answers `false`, as does an API-key session,
|
|
1539
|
+
* which has no plan windows to publish. Only an unknown session is an error,
|
|
1540
|
+
* the same invalid-params error the `_claude/tasks` methods answer.
|
|
1541
|
+
*/
|
|
1542
|
+
async refreshUsage(params) {
|
|
1543
|
+
const { sessionId } = params;
|
|
1544
|
+
const log = (outcome, reason) => this.logger.log(`[usage/refresh] method=${USAGE_REFRESH_METHOD} sessionId=${logValue(sessionId)} outcome=${outcome}` +
|
|
1545
|
+
(reason ? ` reason=${reason}` : ""));
|
|
1546
|
+
const session = Object.hasOwn(this.sessions, sessionId) ? this.sessions[sessionId] : undefined;
|
|
1547
|
+
if (!session) {
|
|
1548
|
+
log("error", "unknown_session");
|
|
1549
|
+
throw RequestError.invalidParams(undefined, `unknown session ${sessionId}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (session.queryClosed) {
|
|
1552
|
+
log("skipped", "session_ended");
|
|
1553
|
+
return { refreshed: false };
|
|
1554
|
+
}
|
|
1555
|
+
let coalesced = true;
|
|
1556
|
+
let pending = session.usageRefreshInFlight;
|
|
1557
|
+
if (!pending) {
|
|
1558
|
+
if (session.turnQueue?.length || session.activeTurn) {
|
|
1559
|
+
log("skipped", "turn_in_flight");
|
|
1560
|
+
return { refreshed: false };
|
|
1561
|
+
}
|
|
1562
|
+
coalesced = false;
|
|
1563
|
+
pending = this.publishAccountUsage(sessionId, session, {
|
|
1564
|
+
cacheMaxAgeMs: USAGE_REFRESH_MAX_SAMPLE_AGE_MS,
|
|
1565
|
+
}).finally(() => {
|
|
1566
|
+
if (session.usageRefreshInFlight === pending) {
|
|
1567
|
+
session.usageRefreshInFlight = undefined;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
session.usageRefreshInFlight = pending;
|
|
1571
|
+
}
|
|
1572
|
+
// `publishAccountUsage` never rejects, so neither does this await.
|
|
1573
|
+
const refreshed = await pending;
|
|
1574
|
+
log(refreshed ? "published" : "nothing", coalesced ? "coalesced" : undefined);
|
|
1575
|
+
return { refreshed };
|
|
1576
|
+
}
|
|
1467
1577
|
async publishGoalFromPrompt(sessionId, prompt, commandUuid) {
|
|
1468
1578
|
const goalUpdate = goalUpdateFromPrompt(prompt);
|
|
1469
1579
|
if (goalUpdate !== undefined) {
|
|
@@ -2115,6 +2225,40 @@ export class ClaudeAcpAgent {
|
|
|
2115
2225
|
};
|
|
2116
2226
|
/** The unsettled in-flight turn owning this prompt uuid, if any. */
|
|
2117
2227
|
const findUnsettledTurn = (uuid) => (session.turnQueue ?? []).find((t) => t.promptUuid === uuid && !t.settled);
|
|
2228
|
+
/** Whether this result is the answer to a prompt that is still waiting.
|
|
2229
|
+
*
|
|
2230
|
+
* Claude Code can start a turn on its own, for example when a background
|
|
2231
|
+
* task finishes. If the user sends a prompt while that turn runs, Claude
|
|
2232
|
+
* Code adds the prompt to the running turn. The turn's result is still
|
|
2233
|
+
* marked as a background result, but it lists the uuids of the prompts it
|
|
2234
|
+
* answered.
|
|
2235
|
+
*
|
|
2236
|
+
* A turn held open for its background subagents already has its result,
|
|
2237
|
+
* so a result that names it does not answer it. */
|
|
2238
|
+
const answersPendingPrompt = (message) => {
|
|
2239
|
+
// Newer CLIs list every prompt the turn answered. Older ones name only
|
|
2240
|
+
// the last one.
|
|
2241
|
+
let answeredPromptUuids = [];
|
|
2242
|
+
if (Array.isArray(message.user_message_uuids)) {
|
|
2243
|
+
answeredPromptUuids = message.user_message_uuids;
|
|
2244
|
+
}
|
|
2245
|
+
else if (typeof message.user_message_uuid === "string") {
|
|
2246
|
+
answeredPromptUuids = [message.user_message_uuid];
|
|
2247
|
+
}
|
|
2248
|
+
for (const promptUuid of answeredPromptUuids) {
|
|
2249
|
+
const turn = findUnsettledTurn(promptUuid);
|
|
2250
|
+
if (turn === undefined) {
|
|
2251
|
+
// Not a prompt of this session, or it was answered already.
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
if (isHeldOpen(turn)) {
|
|
2255
|
+
// This turn already has its result and only waits for its subagents.
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
return false;
|
|
2261
|
+
};
|
|
2118
2262
|
/** The first queued turn still awaiting its outcome, if any — the single
|
|
2119
2263
|
* spelling of "a prompt is pending" shared by the head promotion and
|
|
2120
2264
|
* the autonomous stretch-close guard. */
|
|
@@ -3403,7 +3547,10 @@ export class ClaudeAcpAgent {
|
|
|
3403
3547
|
// the user's prompt's. Autonomous results must never touch the
|
|
3404
3548
|
// user-turn lifecycle (stop reason, settles, failActive,
|
|
3405
3549
|
// slash-command output forwarding), though their cost is real.
|
|
3406
|
-
|
|
3550
|
+
// The exception: the user's prompt was added to that turn while it
|
|
3551
|
+
// ran, so its result answers the prompt (see answersPendingPrompt).
|
|
3552
|
+
const startedByClaudeCode = message.origin != null && AUTONOMOUS_RESULT_ORIGINS.has(message.origin.kind);
|
|
3553
|
+
const isAutonomousResult = startedByClaudeCode && !answersPendingPrompt(message);
|
|
3407
3554
|
const pendingExitPlanModeInterruption = session.pendingExitPlanModeInterruption;
|
|
3408
3555
|
const pendingExitPlanContextReset = session.pendingExitPlanContextReset;
|
|
3409
3556
|
try {
|
|
@@ -4962,6 +5109,18 @@ export class ClaudeAcpAgent {
|
|
|
4962
5109
|
await this.applyFastMode(session, resolveFastModeEnabled(params));
|
|
4963
5110
|
return { configOptions: session.configOptions };
|
|
4964
5111
|
}
|
|
5112
|
+
// Ultracode is a boolean too, and bypasses the same validation. The legacy
|
|
5113
|
+
// `effort = "ultracode"` (the retired notch) is accepted for one release.
|
|
5114
|
+
const useBoolean = clientSupportsBooleanConfigOptions(this.clientCapabilities);
|
|
5115
|
+
if (params.configId === ULTRACODE_CONFIG_ID) {
|
|
5116
|
+
await applyUltracodeToggle(session, resolveUltracodeRequested(params), useBoolean);
|
|
5117
|
+
return { configOptions: session.configOptions };
|
|
5118
|
+
}
|
|
5119
|
+
if (params.configId === EFFORT_CONFIG_ID &&
|
|
5120
|
+
isLegacyUltracodeEffort(params.value) &&
|
|
5121
|
+
(await applyLegacyUltracodeEffort(session, useBoolean))) {
|
|
5122
|
+
return { configOptions: session.configOptions };
|
|
5123
|
+
}
|
|
4965
5124
|
// The Thinking toggle only records the session's intent — the SDK query
|
|
4966
5125
|
// swap is deferred to the next prompt() start (lazy recreate, sub-task
|
|
4967
5126
|
// 1.3), so there is no SDK call to fail here and the config option is
|
|
@@ -5647,7 +5806,10 @@ export class ClaudeAcpAgent {
|
|
|
5647
5806
|
if (session.fastModeDisabledReason === "model_not_allowed") {
|
|
5648
5807
|
session.fastModeDisabledReason = undefined;
|
|
5649
5808
|
}
|
|
5650
|
-
// Rebuild config options since effort levels depend on the selected model
|
|
5809
|
+
// Rebuild config options since effort levels depend on the selected model.
|
|
5810
|
+
// The new model decides whether Ultracode is shown (heuristic now, the
|
|
5811
|
+
// CLI's readback below); the retained intent decides its value.
|
|
5812
|
+
session.ultracodeAvailable = isUltracodeAvailable(newModelInfo?.supportsEffort ? (newModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: session.workflowsDisabled });
|
|
5651
5813
|
const effortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5652
5814
|
const currentEffort = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
|
5653
5815
|
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, currentEffort, session.agents, session.currentAgent, {
|
|
@@ -5666,15 +5828,7 @@ export class ClaudeAcpAgent {
|
|
|
5666
5828
|
// (R1.6).
|
|
5667
5829
|
session.thinkingEnabled ??
|
|
5668
5830
|
effectiveThinkingConfig(undefined, process.env.MAX_THINKING_TOKENS, this.logger) !==
|
|
5669
|
-
undefined, {
|
|
5670
|
-
// The new model decides whether the entry renders at all; the session's
|
|
5671
|
-
// retained intent decides whether it renders selected. A model without
|
|
5672
|
-
// `xhigh` drops the entry, and `validEffort` then falls back for it the
|
|
5673
|
-
// same way it does for any unsupported level.
|
|
5674
|
-
available: isUltracodeAvailable(newModelInfo?.supportsEffort ? (newModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: session.workflowsDisabled }),
|
|
5675
|
-
enabled: session.ultracode,
|
|
5676
|
-
}, {
|
|
5677
|
-
disableWorkflows: session.workflowsDisabled,
|
|
5831
|
+
undefined, this.ultracodeState(session, session.ultracodeAvailable), {
|
|
5678
5832
|
// Threaded for the same reason Thinking is (R1.7): an option not
|
|
5679
5833
|
// passed through this rebuild silently drops from the picker on
|
|
5680
5834
|
// every model switch. The account is model-independent, so the row
|
|
@@ -5686,8 +5840,7 @@ export class ClaudeAcpAgent {
|
|
|
5686
5840
|
const newEffortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5687
5841
|
const newEffort = typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
|
|
5688
5842
|
if (newEffort !== currentEffort) {
|
|
5689
|
-
await session.query.applyFlagSettings(
|
|
5690
|
-
session.ultracode = isUltracodeValue(newEffort);
|
|
5843
|
+
await session.query.applyFlagSettings(effortFlagSettings(newEffort, session.ultracodeRequested));
|
|
5691
5844
|
}
|
|
5692
5845
|
// Emit current_mode_update only after session.modes AND
|
|
5693
5846
|
// session.configOptions have been fully reconciled. This way, a failure
|
|
@@ -5699,8 +5852,10 @@ export class ClaudeAcpAgent {
|
|
|
5699
5852
|
await this.sessionModes.publishFallbackState(sessionId, session);
|
|
5700
5853
|
}
|
|
5701
5854
|
// Last, so the switch's own control requests don't queue behind it.
|
|
5702
|
-
if (modelChanged)
|
|
5855
|
+
if (modelChanged) {
|
|
5703
5856
|
this.refreshContextWindowInBackground(sessionId, session);
|
|
5857
|
+
this.refreshUltracodeInBackground(sessionId, session, false);
|
|
5858
|
+
}
|
|
5704
5859
|
}
|
|
5705
5860
|
else if (configId === AGENT_CONFIG_ID) {
|
|
5706
5861
|
// Live agent switch — no subprocess restart needed. Apply the SDK flag
|
|
@@ -5737,11 +5892,9 @@ export class ClaudeAcpAgent {
|
|
|
5737
5892
|
else {
|
|
5738
5893
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
5739
5894
|
if (configId === EFFORT_CONFIG_ID) {
|
|
5740
|
-
//
|
|
5741
|
-
//
|
|
5742
|
-
|
|
5743
|
-
await session.query.applyFlagSettings(ultracodeFlagSettings(value));
|
|
5744
|
-
session.ultracode = isUltracodeValue(value);
|
|
5895
|
+
// An effort change without `ultracode` turns it off, so the intent
|
|
5896
|
+
// rides along — see `effortFlagSettings`.
|
|
5897
|
+
await session.query.applyFlagSettings(effortFlagSettings(value, session.ultracodeRequested));
|
|
5745
5898
|
}
|
|
5746
5899
|
}
|
|
5747
5900
|
}
|
|
@@ -5794,6 +5947,50 @@ export class ClaudeAcpAgent {
|
|
|
5794
5947
|
session.fastModeEnabled = enabled;
|
|
5795
5948
|
this.refreshFastModeOption(session, enabled);
|
|
5796
5949
|
}
|
|
5950
|
+
/** The Ultracode display state for `buildConfigOptions`. */
|
|
5951
|
+
ultracodeState(session, available) {
|
|
5952
|
+
return {
|
|
5953
|
+
available,
|
|
5954
|
+
requested: session.ultracodeRequested,
|
|
5955
|
+
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
/**
|
|
5959
|
+
* Replace the Ultracode heuristic with the CLI's `applied` report, without
|
|
5960
|
+
* blocking the caller — never inline, because control requests queue ahead
|
|
5961
|
+
* of interrupt and close (upstream #1216). `seedIntent` at session start; a
|
|
5962
|
+
* model switch keeps its intent and only re-reads availability. Deferred a
|
|
5963
|
+
* tick so a resulting `config_option_update` follows the session/new reply.
|
|
5964
|
+
*/
|
|
5965
|
+
refreshUltracodeInBackground(sessionId, session, seedIntent) {
|
|
5966
|
+
const { query } = session;
|
|
5967
|
+
const modelId = session.models.currentModelId;
|
|
5968
|
+
const requestedAtStart = session.ultracodeRequested;
|
|
5969
|
+
const stillCurrent = () => this.sessions[sessionId] === session &&
|
|
5970
|
+
session.query === query &&
|
|
5971
|
+
session.models.currentModelId === modelId;
|
|
5972
|
+
setTimeout(() => {
|
|
5973
|
+
void readUltracodeState(query, (error) => {
|
|
5974
|
+
this.logger.log(`[ultracode] get_settings failed session=${sessionId}: ${String(error)}`);
|
|
5975
|
+
})
|
|
5976
|
+
.then(async (readback) => {
|
|
5977
|
+
if (!readback || !stillCurrent())
|
|
5978
|
+
return;
|
|
5979
|
+
// A toggle that landed while the read was in flight is newer than it.
|
|
5980
|
+
const seed = seedIntent && session.ultracodeRequested === requestedAtStart;
|
|
5981
|
+
const useBoolean = clientSupportsBooleanConfigOptions(this.clientCapabilities);
|
|
5982
|
+
if (!applyUltracodeReadback(session, readback, seed, useBoolean))
|
|
5983
|
+
return;
|
|
5984
|
+
await this.client.sessionUpdate({
|
|
5985
|
+
sessionId,
|
|
5986
|
+
update: { sessionUpdate: "config_option_update", configOptions: session.configOptions },
|
|
5987
|
+
});
|
|
5988
|
+
})
|
|
5989
|
+
.catch((error) => {
|
|
5990
|
+
this.logger.error(`[ultracode] readback failed session=${sessionId}:`, error);
|
|
5991
|
+
});
|
|
5992
|
+
}, 0);
|
|
5993
|
+
}
|
|
5797
5994
|
/** Lazily swap a session's SDK query for a fresh one so a Thinking change
|
|
5798
5995
|
* takes effect on the next turn (R1.3): thinking has no live flag-settings
|
|
5799
5996
|
* path in the SDK, so mid-session application goes through query recreation
|
|
@@ -5915,14 +6112,16 @@ export class ClaudeAcpAgent {
|
|
|
5915
6112
|
// await is already included.
|
|
5916
6113
|
const effortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5917
6114
|
const effortValue = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
|
5918
|
-
const
|
|
5919
|
-
// Only the positive
|
|
6115
|
+
const effortFlags = effortFlagSettings(effortValue, session.ultracodeRequested);
|
|
6116
|
+
// Only the positive cases are re-applied: the replacement's flag layer
|
|
5920
6117
|
// starts empty, so clearing a key that was never set would be a control
|
|
5921
|
-
// request that says nothing.
|
|
5922
|
-
//
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
6118
|
+
// request that says nothing. The Ultracode intent is replayed even at
|
|
6119
|
+
// Default effort.
|
|
6120
|
+
if (effortFlags.effortLevel !== null) {
|
|
6121
|
+
await replacement.applyFlagSettings(effortFlags);
|
|
6122
|
+
}
|
|
6123
|
+
else if (effortFlags.ultracode) {
|
|
6124
|
+
await replacement.applyFlagSettings({ ultracode: true });
|
|
5926
6125
|
}
|
|
5927
6126
|
const supportsFastMode = session.modelInfos.find((m) => m.value === session.models.currentModelId)
|
|
5928
6127
|
?.supportsFastMode ?? false;
|
|
@@ -6704,6 +6903,7 @@ export class ClaudeAcpAgent {
|
|
|
6704
6903
|
disabledReason: fastModeDisabledReason,
|
|
6705
6904
|
};
|
|
6706
6905
|
const workflowsDisabled = settingsManager.getSettings().disableWorkflows === true;
|
|
6906
|
+
const ultracodeRequested = settingsManager.getSettings().ultracode === true;
|
|
6707
6907
|
const configOptions = buildConfigOptions(modes, models,
|
|
6708
6908
|
// Catalog-based (see `modelInfos` above), matching the model-switch
|
|
6709
6909
|
// rebuild (which passes `session.modelInfos`): `buildConfigOptions`
|
|
@@ -6717,31 +6917,24 @@ export class ClaudeAcpAgent {
|
|
|
6717
6917
|
// effectiveThinkingConfig(undefined, MAX_THINKING_TOKENS) from above, so
|
|
6718
6918
|
// reusing it avoids re-parsing (and re-logging an invalid) env var —
|
|
6719
6919
|
// exactly one error log per query creation.
|
|
6720
|
-
thinking !== undefined,
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
available: isUltracodeAvailable(currentModelInfo?.supportsEffort ? (currentModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: workflowsDisabled }),
|
|
6725
|
-
enabled: settingsManager.getSettings().ultracode === true,
|
|
6726
|
-
}, {
|
|
6727
|
-
disableWorkflows: workflowsDisabled,
|
|
6920
|
+
thinking !== undefined,
|
|
6921
|
+
// Seeded from settings and the heuristic; the CLI's own report replaces
|
|
6922
|
+
// both in the background (`refreshUltracodeInBackground`, below).
|
|
6923
|
+
this.ultracodeState({ ultracodeRequested }, isUltracodeAvailable(currentModelInfo?.supportsEffort ? (currentModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: workflowsDisabled })), {
|
|
6728
6924
|
// Both halves of the selector, resolved above alongside the env they
|
|
6729
6925
|
// produced — so the row the client renders and the configuration
|
|
6730
6926
|
// directory the query was actually started with cannot disagree.
|
|
6731
6927
|
accounts: declaredAccounts,
|
|
6732
6928
|
currentAccount: accountEntry?.value,
|
|
6733
6929
|
});
|
|
6734
|
-
// Apply the initial effort level to the SDK so it matches the UI default
|
|
6930
|
+
// Apply the initial effort level to the SDK so it matches the UI default.
|
|
6931
|
+
// It carries the Ultracode intent: an effort change without the key would
|
|
6932
|
+
// turn off an `ultracode: true` the session was started with.
|
|
6735
6933
|
const initialEffort = configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
6736
|
-
// The picker's resolved value, not the raw setting: `buildConfigOptions`
|
|
6737
|
-
// has already dropped Ultracode if this model cannot reach `xhigh`, and the
|
|
6738
|
-
// session must track what is displayed rather than what was requested.
|
|
6739
|
-
const initialUltracode = typeof initialEffort?.currentValue === "string" &&
|
|
6740
|
-
isUltracodeValue(initialEffort.currentValue);
|
|
6741
6934
|
if (initialEffort &&
|
|
6742
6935
|
typeof initialEffort.currentValue === "string" &&
|
|
6743
6936
|
initialEffort.currentValue !== "default") {
|
|
6744
|
-
await q.applyFlagSettings(
|
|
6937
|
+
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, ultracodeRequested));
|
|
6745
6938
|
}
|
|
6746
6939
|
// Seed the context window WITHOUT any extra IPC on the session/new path.
|
|
6747
6940
|
// On session/load, the resumed session's own `getContextUsage` report — a
|
|
@@ -6806,7 +6999,8 @@ export class ClaudeAcpAgent {
|
|
|
6806
6999
|
configOptions,
|
|
6807
7000
|
agents,
|
|
6808
7001
|
currentAgent,
|
|
6809
|
-
|
|
7002
|
+
ultracodeRequested,
|
|
7003
|
+
ultracodeAvailable: configOptions.some((o) => o.id === ULTRACODE_CONFIG_ID),
|
|
6810
7004
|
workflowsDisabled,
|
|
6811
7005
|
declaredAccounts,
|
|
6812
7006
|
currentAccount: accountEntry?.value,
|
|
@@ -6854,6 +7048,7 @@ export class ClaudeAcpAgent {
|
|
|
6854
7048
|
// A guessed window seed (text heuristic / default) is refined the same way,
|
|
6855
7049
|
// unawaited; a resumed session's authoritative seed makes it a no-op.
|
|
6856
7050
|
this.refreshContextWindowInBackground(sessionId, this.sessions[sessionId]);
|
|
7051
|
+
this.refreshUltracodeInBackground(sessionId, this.sessions[sessionId], true);
|
|
6857
7052
|
return {
|
|
6858
7053
|
sessionId,
|
|
6859
7054
|
modes,
|
|
@@ -7187,16 +7382,11 @@ export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel
|
|
|
7187
7382
|
* Both session call sites always supply it (R1.1); `undefined` (direct
|
|
7188
7383
|
* callers/tests) omits the row. */
|
|
7189
7384
|
thinkingEnabled,
|
|
7190
|
-
/** Display state for the Ultracode
|
|
7191
|
-
* direct callers/tests, in which case
|
|
7192
|
-
* `settings` and the entry renders unselected. */
|
|
7385
|
+
/** Display state for the Ultracode option, placed after effort. Omitted by
|
|
7386
|
+
* direct callers/tests, in which case the option is not shown. */
|
|
7193
7387
|
ultracode,
|
|
7194
|
-
/**
|
|
7195
|
-
*
|
|
7196
|
-
* declared accounts plus the one in force, which decide whether an account
|
|
7197
|
-
* selector is advertised at all (R6.1/R6.2). `accounts` is not a field of
|
|
7198
|
-
* the SDK's `Settings`, so it is carried beside them rather than picked
|
|
7199
|
-
* from them. */
|
|
7388
|
+
/** The declared accounts plus the one in force, which decide whether an
|
|
7389
|
+
* account selector is advertised at all (R6.1/R6.2). */
|
|
7200
7390
|
settings) {
|
|
7201
7391
|
const options = [
|
|
7202
7392
|
SessionModeManager.configOption(modes),
|
|
@@ -7230,11 +7420,6 @@ settings) {
|
|
|
7230
7420
|
? (currentModelInfo.supportedEffortLevels ?? [])
|
|
7231
7421
|
: [];
|
|
7232
7422
|
if (supportedLevels.length > 0) {
|
|
7233
|
-
// Ultracode rides the effort picker as one extra entry past the last level,
|
|
7234
|
-
// which is where the VS Code extension draws it too. It is NOT a level (see
|
|
7235
|
-
// `ultracode.ts`): the entry stands for `xhigh` plus workflow orchestration,
|
|
7236
|
-
// and it is offered only when both halves are reachable.
|
|
7237
|
-
const ultracodeAvailable = ultracode?.available ?? isUltracodeAvailable(supportedLevels, settings);
|
|
7238
7423
|
const effortOptions = [
|
|
7239
7424
|
{ value: "default", name: "Default" },
|
|
7240
7425
|
...supportedLevels.map((level) => ({
|
|
@@ -7244,21 +7429,9 @@ settings) {
|
|
|
7244
7429
|
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
7245
7430
|
.join(" "),
|
|
7246
7431
|
})),
|
|
7247
|
-
...(ultracodeAvailable
|
|
7248
|
-
? [{ value: ULTRACODE_OPTION_VALUE, name: ULTRACODE_OPTION_NAME }]
|
|
7249
|
-
: []),
|
|
7250
7432
|
];
|
|
7251
|
-
const includes = (l) => l === "default" ||
|
|
7252
|
-
|
|
7253
|
-
(ultracodeAvailable && l === ULTRACODE_OPTION_VALUE);
|
|
7254
|
-
// `effortOptionValue` resolves the flag against the level BEFORE validation,
|
|
7255
|
-
// so an Ultracode session on a model that dropped `xhigh` falls back to
|
|
7256
|
-
// "Default" through the same path any unsupported level does.
|
|
7257
|
-
const displayedEffort = effortOptionValue(currentEffortLevel, {
|
|
7258
|
-
available: ultracodeAvailable,
|
|
7259
|
-
enabled: ultracode?.enabled ?? false,
|
|
7260
|
-
});
|
|
7261
|
-
const validEffort = displayedEffort && includes(displayedEffort) ? displayedEffort : "default";
|
|
7433
|
+
const includes = (l) => l === "default" || supportedLevels.includes(l);
|
|
7434
|
+
const validEffort = currentEffortLevel && includes(currentEffortLevel) ? currentEffortLevel : "default";
|
|
7262
7435
|
options.push({
|
|
7263
7436
|
id: EFFORT_CONFIG_ID,
|
|
7264
7437
|
name: "Effort",
|
|
@@ -7269,6 +7442,11 @@ settings) {
|
|
|
7269
7442
|
options: effortOptions,
|
|
7270
7443
|
});
|
|
7271
7444
|
}
|
|
7445
|
+
// Ultracode: a toggle beside effort, not an effort level (`ultracode.ts`).
|
|
7446
|
+
const ultracodeOption = ultracodeConfigOption(ultracode);
|
|
7447
|
+
if (ultracodeOption) {
|
|
7448
|
+
options.push(ultracodeOption);
|
|
7449
|
+
}
|
|
7272
7450
|
// Surface the Fast mode toggle only when the current model supports it. The
|
|
7273
7451
|
// option renders as a native boolean toggle for Clients that opted in, and a
|
|
7274
7452
|
// two-value select otherwise.
|
|
@@ -8724,6 +8902,7 @@ export function runAcp(logger) {
|
|
|
8724
8902
|
.onRequest(TASKS_STOP_METHOD, { parse: parseTasksStopRequest }, (ctx) => agent.stopTask(ctx.params))
|
|
8725
8903
|
.onRequest(TASKS_OUTPUT_METHOD, { parse: parseTasksOutputRequest }, (ctx) => agent.taskOutput(ctx.params))
|
|
8726
8904
|
.onRequest(TASKS_BACKGROUND_METHOD, { parse: parseTasksBackgroundRequest }, (ctx) => agent.backgroundTask(ctx.params))
|
|
8905
|
+
.onRequest(USAGE_REFRESH_METHOD, { parse: parseUsageRefreshRequest }, (ctx) => agent.refreshUsage(ctx.params))
|
|
8727
8906
|
.onRequest(GOAL_CONTROL_METHOD, { parse: parseGoalRequest }, (ctx) => agent.goal(ctx.params))
|
|
8728
8907
|
.connect(stream);
|
|
8729
8908
|
agent = new ClaudeAcpAgent(new ClientConnection(connection.client), logger);
|