@lucascouts/claude-agent-acp-plus 0.11.1 → 0.13.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/accounts.d.ts +143 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +196 -0
- package/dist/acp-agent.d.ts +76 -2
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +395 -94
- package/dist/context-compaction-meta.d.ts +73 -0
- package/dist/context-compaction-meta.d.ts.map +1 -0
- package/dist/context-compaction-meta.js +53 -0
- package/dist/context-compaction.d.ts +160 -0
- package/dist/context-compaction.d.ts.map +1 -0
- package/dist/context-compaction.js +275 -0
- package/dist/ultracode.d.ts +100 -0
- package/dist/ultracode.d.ts.map +1 -0
- package/dist/ultracode.js +103 -0
- package/dist/usage-markdown.d.ts +92 -0
- package/dist/usage-markdown.d.ts.map +1 -0
- package/dist/usage-markdown.js +373 -0
- package/package.json +9 -9
package/dist/acp-agent.js
CHANGED
|
@@ -14,7 +14,9 @@ import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createEli
|
|
|
14
14
|
import { agentName } from "./agent-name.js";
|
|
15
15
|
import { filterDeprecatedModels } from "./model-deprecation.js";
|
|
16
16
|
import { SettingsManager } from "./settings.js";
|
|
17
|
+
import { ContextCompactionLifecycle, contextCompactionMetadataFromBoundary, } from "./context-compaction.js";
|
|
17
18
|
import { createThinkingConfigOption, effectiveThinkingConfig, resolveThinkingSelection, THINKING_CONFIG_ID, } from "./thinking-option.js";
|
|
19
|
+
import { effortOptionValue, isUltracodeAvailable, isUltracodeValue, ULTRACODE_OPTION_NAME, ULTRACODE_OPTION_VALUE, ultracodeFlagSettings, } from "./ultracode.js";
|
|
18
20
|
import { handleRewindCommand, parseRewindInvocation } from "./rewind-command.js";
|
|
19
21
|
import { ALLOW_BYPASS, resolvePermissionMode } from "./permissions/modes.js";
|
|
20
22
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
@@ -29,6 +31,8 @@ import { acceptedPlanToolResult, ExitPlanCoordinator, executionDiagnostic, exitP
|
|
|
29
31
|
import { DEFAULT_AGENT_ID, EFFORT_CONFIG_ID } from "./session-config-ids.js";
|
|
30
32
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
31
33
|
import { AccountUsageTracker } from "./account-usage.js";
|
|
34
|
+
import { ACCOUNT_CONFIG_ID, accountForValue, accountInForce, createAccountConfigOption, envForAccount, readDeclaredAccounts, } from "./accounts.js";
|
|
35
|
+
import { isUsageCommandText, structuredUsageMarkdown } from "./usage-markdown.js";
|
|
32
36
|
export { DEFAULT_AGENT_ID, EFFORT_CONFIG_ID } from "./session-config-ids.js";
|
|
33
37
|
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
|
|
34
38
|
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
@@ -480,6 +484,14 @@ export class ClaudeAcpAgent {
|
|
|
480
484
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
481
485
|
* tests can shrink it. */
|
|
482
486
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
487
|
+
/** The account selector's last choice, as a selector value (see
|
|
488
|
+
* `accounts.ts`). Held on the AGENT, not on a session, because that is what
|
|
489
|
+
* it governs: the account applies to sessions created after it is set, and
|
|
490
|
+
* the thread that was on screen when the user chose keeps the account it
|
|
491
|
+
* started under (D7 — a session id belongs to one account's local history,
|
|
492
|
+
* so a live thread cannot be carried across a switch). `undefined` until a
|
|
493
|
+
* selection is made, in which case the first declared account stands. */
|
|
494
|
+
selectedAccount;
|
|
483
495
|
constructor(client, logger) {
|
|
484
496
|
this.sessions = {};
|
|
485
497
|
this.client = client;
|
|
@@ -1006,6 +1018,12 @@ export class ClaudeAcpAgent {
|
|
|
1006
1018
|
session.fileChangeReportRequestIds.add(fileChangeReportRequestId);
|
|
1007
1019
|
fileChangeAudit = createFileChangeAuditTurnState(fileChangeReportRequestId);
|
|
1008
1020
|
}
|
|
1021
|
+
// R2.3: exactly `/usage`, and nothing else in the prompt. A second block
|
|
1022
|
+
// (an attached file, a second text part) is a model turn that happens to
|
|
1023
|
+
// mention the command, not the local command itself.
|
|
1024
|
+
const isUsageCommand = params.prompt.length === 1 &&
|
|
1025
|
+
params.prompt[0]?.type === "text" &&
|
|
1026
|
+
isUsageCommandText(params.prompt[0].text);
|
|
1009
1027
|
session.titles.onPrompt(params.prompt);
|
|
1010
1028
|
// Each prompt is a Turn whose deferred the persistent consumer settles once
|
|
1011
1029
|
// the turn's outcome is known. `prompt()` owns no loop: it enqueues the
|
|
@@ -1014,6 +1032,9 @@ export class ClaudeAcpAgent {
|
|
|
1014
1032
|
const turn = {
|
|
1015
1033
|
promptUuid,
|
|
1016
1034
|
isLocalOnlyCommand,
|
|
1035
|
+
...(isUsageCommand
|
|
1036
|
+
? { isUsageCommand: true, usageMarkdownAbort: new AbortController() }
|
|
1037
|
+
: {}),
|
|
1017
1038
|
...(fileChangeAudit ? { fileChangeAudit } : {}),
|
|
1018
1039
|
settled: false,
|
|
1019
1040
|
resolve: () => { },
|
|
@@ -1309,11 +1330,6 @@ export class ClaudeAcpAgent {
|
|
|
1309
1330
|
// stop_reason "refusal" and structured stop_details. We capture the
|
|
1310
1331
|
// human-readable explanation so the terminal `result` can surface it.
|
|
1311
1332
|
let lastRefusalExplanation = null;
|
|
1312
|
-
// Tracks whether we're inside a compaction. The SDK emits the terminal
|
|
1313
|
-
// `status` (compact_result success/failed) twice for a single failed
|
|
1314
|
-
// compaction, and the two messages are indistinguishable — so we report the
|
|
1315
|
-
// outcome only while a compaction is in progress, then clear this.
|
|
1316
|
-
let compactionInProgress = false;
|
|
1317
1333
|
// Anthropic API message id of the assistant message currently being
|
|
1318
1334
|
// streamed, captured from `message_start` so the streamed chunks that follow
|
|
1319
1335
|
// (whose delta events don't carry it) can all be tagged with the same,
|
|
@@ -1348,6 +1364,12 @@ export class ClaudeAcpAgent {
|
|
|
1348
1364
|
* recognizable by the `parentToolUseId` meta that toAcpNotifications
|
|
1349
1365
|
* stamps from `parent_tool_use_id`, and never reach the top-level feed
|
|
1350
1366
|
* as the turn's answer. */
|
|
1367
|
+
/** Compaction as one ACP tool lifecycle, replacing the in-progress boolean
|
|
1368
|
+
* this consumer used to infer it from (story 010, R2.1/R2.2). Declared
|
|
1369
|
+
* ahead of `sendUpdate` because that chokepoint consults it; the closure
|
|
1370
|
+
* below only dereferences `sendUpdate` when an update is actually sent,
|
|
1371
|
+
* which is always after both bindings exist. */
|
|
1372
|
+
const compaction = new ContextCompactionLifecycle((notification) => sendUpdate(notification));
|
|
1351
1373
|
const sendUpdate = async (notification) => {
|
|
1352
1374
|
const { update } = notification;
|
|
1353
1375
|
if (isFileChangeAuditReportPhase(session.activeTurn?.fileChangeAudit) &&
|
|
@@ -1360,6 +1382,14 @@ export class ClaudeAcpAgent {
|
|
|
1360
1382
|
}
|
|
1361
1383
|
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1362
1384
|
const claudeMeta = update._meta?.claudeCode;
|
|
1385
|
+
// A failed manual compaction's error also arrives as assistant text;
|
|
1386
|
+
// the tool lifecycle already carried it, so drop that one copy rather
|
|
1387
|
+
// than report the same failure twice (R2.2).
|
|
1388
|
+
if (!claudeMeta?.parentToolUseId &&
|
|
1389
|
+
update.content.type === "text" &&
|
|
1390
|
+
compaction.consumeDuplicateErrorOutput(update.content.text)) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1363
1393
|
if (!claudeMeta?.parentToolUseId) {
|
|
1364
1394
|
session.emittedAssistantText = true;
|
|
1365
1395
|
session.titles.onAssistantText(update.content);
|
|
@@ -1412,7 +1442,6 @@ export class ClaudeAcpAgent {
|
|
|
1412
1442
|
lastAssistantWasUsageLimit = false;
|
|
1413
1443
|
lastAssistantFailureTitle = undefined;
|
|
1414
1444
|
lastRefusalExplanation = null;
|
|
1415
|
-
compactionInProgress = false;
|
|
1416
1445
|
// Do NOT reset currentStreamMessageId or streamedBlocks here. Turn
|
|
1417
1446
|
// activation can fire mid-message (the replayed user echo with
|
|
1418
1447
|
// --replay-user-messages lands between a message's blocks); clearing the
|
|
@@ -1431,6 +1460,22 @@ export class ClaudeAcpAgent {
|
|
|
1431
1460
|
if (session.activeTurn)
|
|
1432
1461
|
session.activeTurn.carriedUsage = undefined;
|
|
1433
1462
|
};
|
|
1463
|
+
/** Start this turn's structured `/usage` render, once, and hand back the
|
|
1464
|
+
* in-flight promise; undefined when the turn owns no render.
|
|
1465
|
+
*
|
|
1466
|
+
* Started at ACTIVATION rather than when the output arrives, so the
|
|
1467
|
+
* bounded wait overlaps the command instead of following it: by the time
|
|
1468
|
+
* the CLI has printed its answer the report has usually already lost or
|
|
1469
|
+
* won its race, and the user waits for neither. */
|
|
1470
|
+
const ensureUsageMarkdown = (turn) => {
|
|
1471
|
+
if (!turn.isUsageCommand || !turn.usageMarkdownAbort) {
|
|
1472
|
+
return undefined;
|
|
1473
|
+
}
|
|
1474
|
+
// `session.query` is read here rather than captured: a provider switch
|
|
1475
|
+
// can replace it, and the render must ask the query that is live now.
|
|
1476
|
+
turn.usageMarkdown ??= structuredUsageMarkdown(session.query, turn.usageMarkdownAbort.signal, this.logger);
|
|
1477
|
+
return turn.usageMarkdown;
|
|
1478
|
+
};
|
|
1434
1479
|
/** Promote a queued turn to active: it becomes the one output is attributed
|
|
1435
1480
|
* to, and its scratch starts fresh. Clears the cancelled flag so a turn
|
|
1436
1481
|
* enqueued after a prior cancel isn't treated as cancelled. Also clears any
|
|
@@ -1442,6 +1487,7 @@ export class ClaudeAcpAgent {
|
|
|
1442
1487
|
const activateTurn = (turn) => {
|
|
1443
1488
|
session.activeTurn = turn;
|
|
1444
1489
|
session.cancelled = false;
|
|
1490
|
+
ensureUsageMarkdown(turn);
|
|
1445
1491
|
session.pendingOrphanResults = 0;
|
|
1446
1492
|
session.orphanCommands?.clear();
|
|
1447
1493
|
// Two-phase sweep of registry entries the level signal ended (see
|
|
@@ -1480,12 +1526,20 @@ export class ClaudeAcpAgent {
|
|
|
1480
1526
|
*
|
|
1481
1527
|
* But an echo-less result can also be an ORPHAN: cancel() settles+removes a
|
|
1482
1528
|
* queued turn whose user message was already pushed, so the SDK still runs
|
|
1483
|
-
* it and emits a result with no
|
|
1529
|
+
* it and emits a result with no echo to match. Promoting the head for an
|
|
1484
1530
|
* orphan would misattribute its stop reason/usage to an unrelated later
|
|
1485
1531
|
* prompt. `session.pendingOrphanResults` counts exactly how many such
|
|
1486
1532
|
* orphans are still expected (FIFO, they arrive before any live turn's
|
|
1487
|
-
* result), so we skip those and only promote once the count is drained.
|
|
1488
|
-
|
|
1533
|
+
* result), so we skip those and only promote once the count is drained.
|
|
1534
|
+
*
|
|
1535
|
+
* `resultUserMessageUuid` is the result's own join key (SDK 0.3.246+
|
|
1536
|
+
* echoes the triggering send's client uuid on results; absent on older
|
|
1537
|
+
* CLIs, synthetic/meta turns, and session-scoped failures). When present
|
|
1538
|
+
* it upgrades the map lane from positional heuristics to an exact match:
|
|
1539
|
+
* a stamp naming an orphaned command consumes the result outright, and a
|
|
1540
|
+
* stamp naming anything else positively refutes "this is a dead turn's
|
|
1541
|
+
* result", so the dup-over-loss one-skip must not eat it. */
|
|
1542
|
+
const ensureActiveTurn = (resultUserMessageUuid) => {
|
|
1489
1543
|
if (session.activeTurn) {
|
|
1490
1544
|
if (!isHeldOpen(session.activeTurn)) {
|
|
1491
1545
|
return;
|
|
@@ -1537,8 +1591,19 @@ export class ClaudeAcpAgent {
|
|
|
1537
1591
|
// double-consume it. The unexpected-transition logging in the frame
|
|
1538
1592
|
// handler is the tripwire for that class of drift.
|
|
1539
1593
|
if (session.orphanCommands?.size) {
|
|
1594
|
+
// Resolved BEFORE the drain below, which deletes every started/zombie
|
|
1595
|
+
// entry: a stamp naming a "started" orphan would no longer be found
|
|
1596
|
+
// afterwards, and the result would promote the head — misattributing
|
|
1597
|
+
// a dead turn's outcome to a live prompt, the exact failure the map
|
|
1598
|
+
// exists to prevent. The lookup order is load-bearing.
|
|
1599
|
+
const stampedOrphanUuid = resultUserMessageUuid !== undefined && session.orphanCommands.has(resultUserMessageUuid)
|
|
1600
|
+
? resultUserMessageUuid
|
|
1601
|
+
: undefined;
|
|
1540
1602
|
let consumedOrphanResult = false;
|
|
1541
1603
|
let oldestPending;
|
|
1604
|
+
// The started/zombie drain applies regardless of the stamp: commands
|
|
1605
|
+
// folded into the turn that emitted this result share it, and zombies'
|
|
1606
|
+
// late results have already passed (or never existed).
|
|
1542
1607
|
for (const [uuid, state] of session.orphanCommands) {
|
|
1543
1608
|
if (state === "started" || state === "zombie") {
|
|
1544
1609
|
consumedOrphanResult = true;
|
|
@@ -1548,17 +1613,33 @@ export class ClaudeAcpAgent {
|
|
|
1548
1613
|
oldestPending ??= uuid;
|
|
1549
1614
|
}
|
|
1550
1615
|
}
|
|
1551
|
-
if (
|
|
1616
|
+
if (stampedOrphanUuid !== undefined) {
|
|
1617
|
+
// Exact join: the result names an orphaned command. Delete the
|
|
1618
|
+
// matched entry even when it is still "pending" (its dispatch frame
|
|
1619
|
+
// was lost) and consume the result — no promotion.
|
|
1620
|
+
session.orphanCommands.delete(stampedOrphanUuid);
|
|
1552
1621
|
return;
|
|
1553
1622
|
}
|
|
1554
|
-
if (
|
|
1555
|
-
//
|
|
1556
|
-
// live turn's
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
|
|
1561
|
-
|
|
1623
|
+
if (resultUserMessageUuid !== undefined) {
|
|
1624
|
+
// The stamp names a send that is NOT in the orphan map, so this is
|
|
1625
|
+
// a live turn's result: skip both the consumed-return (its folded
|
|
1626
|
+
// orphans were drained above, but the result itself still needs a
|
|
1627
|
+
// turn) and the dup-over-loss one-skip the stamp refutes, and fall
|
|
1628
|
+
// through to promote the head.
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
if (consumedOrphanResult) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (oldestPending !== undefined) {
|
|
1635
|
+
// No dispatch was seen before this result, so it is very likely a
|
|
1636
|
+
// live turn's — but a lost "started" frame would mean it IS the
|
|
1637
|
+
// orphan's (dup-over-loss: prefer one wrong skip over
|
|
1638
|
+
// misattributing a dead turn's outcome to a live prompt). Grant
|
|
1639
|
+
// each pending entry exactly one skip, like the count lane did.
|
|
1640
|
+
session.orphanCommands.delete(oldestPending);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1562
1643
|
}
|
|
1563
1644
|
}
|
|
1564
1645
|
const head = firstUnsettledQueuedTurn();
|
|
@@ -1599,6 +1680,49 @@ export class ClaudeAcpAgent {
|
|
|
1599
1680
|
* spelling of "a prompt is pending" shared by the head promotion and
|
|
1600
1681
|
* the autonomous stretch-close guard. */
|
|
1601
1682
|
const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
|
|
1683
|
+
/** Claim the structured render for whichever turn is producing this local
|
|
1684
|
+
* command output. Three answers, and the caller must distinguish all
|
|
1685
|
+
* three:
|
|
1686
|
+
*
|
|
1687
|
+
* undefined — not a `/usage` turn, or one of R2.4's three ways out
|
|
1688
|
+
* fired: publish `originalOutput` unchanged
|
|
1689
|
+
* string — the render: publish it INSTEAD of `originalOutput`
|
|
1690
|
+
* null — publish nothing (a duplicate of an already-replaced
|
|
1691
|
+
* frame, or a turn cancelled during the wait)
|
|
1692
|
+
*
|
|
1693
|
+
* No content signature and no text parsing: which turn owns a render was
|
|
1694
|
+
* decided from the prompt, so a command whose output happens to look like
|
|
1695
|
+
* `/usage`'s can never be rewritten. */
|
|
1696
|
+
const takeUsageMarkdown = async (originalOutput) => {
|
|
1697
|
+
const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
1698
|
+
if (!turn) {
|
|
1699
|
+
return undefined;
|
|
1700
|
+
}
|
|
1701
|
+
const pending = ensureUsageMarkdown(turn);
|
|
1702
|
+
if (!pending) {
|
|
1703
|
+
return undefined;
|
|
1704
|
+
}
|
|
1705
|
+
const markdown = await pending;
|
|
1706
|
+
// That await can span the whole bounded wait, and a cancel landing inside
|
|
1707
|
+
// it ends the turn. A cancelled turn publishes nothing — not the render,
|
|
1708
|
+
// and not the original text either.
|
|
1709
|
+
if (session.cancelled) {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
if (markdown === null) {
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
if (turn.usageMarkdownDelivered) {
|
|
1716
|
+
// One local command can reach the client through more than one SDK
|
|
1717
|
+
// message shape. Suppress an exact mirror of the frame already
|
|
1718
|
+
// replaced, but let a later, genuinely different frame (an
|
|
1719
|
+
// interruption diagnostic, say) take the normal path.
|
|
1720
|
+
return turn.usageOriginalOutput === originalOutput ? null : undefined;
|
|
1721
|
+
}
|
|
1722
|
+
turn.usageMarkdownDelivered = true;
|
|
1723
|
+
turn.usageOriginalOutput = originalOutput;
|
|
1724
|
+
return markdown;
|
|
1725
|
+
};
|
|
1602
1726
|
/** Whether any background subagent this turn spawned is still live —
|
|
1603
1727
|
* while true, the turn's settlement stays deferred so the subagent's
|
|
1604
1728
|
* output and permission requests land inside it (see
|
|
@@ -1667,6 +1791,9 @@ export class ClaudeAcpAgent {
|
|
|
1667
1791
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
1668
1792
|
const wasHeld = isHeldOpen(turn);
|
|
1669
1793
|
turn.settled = true;
|
|
1794
|
+
// The turn is over: abandon any structured `/usage` render still in
|
|
1795
|
+
// flight rather than leaving it to run out its own clock (D5).
|
|
1796
|
+
turn.usageMarkdownAbort?.abort();
|
|
1670
1797
|
disarmForceCancel(session);
|
|
1671
1798
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
1672
1799
|
session.activeTurn = null;
|
|
@@ -1699,6 +1826,7 @@ export class ClaudeAcpAgent {
|
|
|
1699
1826
|
}
|
|
1700
1827
|
this.finishFileChangeAudit(session, turn, "providerError");
|
|
1701
1828
|
turn.settled = true;
|
|
1829
|
+
turn.usageMarkdownAbort?.abort();
|
|
1702
1830
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
1703
1831
|
session.activeTurn = null;
|
|
1704
1832
|
streamedToolInputs.clear();
|
|
@@ -1747,6 +1875,7 @@ export class ClaudeAcpAgent {
|
|
|
1747
1875
|
this.finishFileChangeAudit(session, turn, "providerError");
|
|
1748
1876
|
const wasHeld = isHeldOpen(turn);
|
|
1749
1877
|
turn.settled = true;
|
|
1878
|
+
turn.usageMarkdownAbort?.abort();
|
|
1750
1879
|
if (wasHeld) {
|
|
1751
1880
|
// A held turn's answer already streamed and its outcome is
|
|
1752
1881
|
// recorded — a stream death during the post-answer hold is a
|
|
@@ -2072,50 +2201,43 @@ export class ClaudeAcpAgent {
|
|
|
2072
2201
|
}
|
|
2073
2202
|
break;
|
|
2074
2203
|
case "status": {
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2204
|
+
// Compaction is reported as ONE tool lifecycle rather than the
|
|
2205
|
+
// assistant-text banners this branch used to emit (R2.1). The
|
|
2206
|
+
// banners needed a boolean in-progress guard because the SDK
|
|
2207
|
+
// repeats the terminal `status` and the two copies are
|
|
2208
|
+
// indistinguishable here; the lifecycle owns that de-duplication
|
|
2209
|
+
// by phase instead, so the guard is gone rather than inert (R2.2).
|
|
2210
|
+
//
|
|
2211
|
+
// The SDK signals manual `/compact` completion with a status
|
|
2212
|
+
// message carrying `compact_result`, not the `compact_boundary`
|
|
2213
|
+
// message (which only fires when there's content to compact) —
|
|
2214
|
+
// so both frames must be able to close a lifecycle.
|
|
2079
2215
|
if (message.status === "compacting") {
|
|
2080
|
-
|
|
2081
|
-
await sendUpdate({
|
|
2082
|
-
sessionId: message.session_id,
|
|
2083
|
-
update: {
|
|
2084
|
-
sessionUpdate: "agent_message_chunk",
|
|
2085
|
-
content: { type: "text", text: "Compacting..." },
|
|
2086
|
-
},
|
|
2087
|
-
});
|
|
2216
|
+
await compaction.start(message.session_id, message.uuid);
|
|
2088
2217
|
}
|
|
2089
|
-
else if (message.compact_result === "success"
|
|
2090
|
-
|
|
2091
|
-
// message carrying `compact_result`, not the `compact_boundary`
|
|
2092
|
-
// message (which only fires when there's content to compact).
|
|
2093
|
-
compactionInProgress = false;
|
|
2094
|
-
await sendUpdate({
|
|
2095
|
-
sessionId: message.session_id,
|
|
2096
|
-
update: {
|
|
2097
|
-
sessionUpdate: "agent_message_chunk",
|
|
2098
|
-
content: { type: "text", text: "\n\nCompacting completed." },
|
|
2099
|
-
},
|
|
2100
|
-
});
|
|
2218
|
+
else if (message.compact_result === "success") {
|
|
2219
|
+
await compaction.finish(message.session_id, message.uuid, "completed");
|
|
2101
2220
|
}
|
|
2102
|
-
else if (message.compact_result === "failed"
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
await sendUpdate({
|
|
2106
|
-
sessionId: message.session_id,
|
|
2107
|
-
update: {
|
|
2108
|
-
sessionUpdate: "agent_message_chunk",
|
|
2109
|
-
content: { type: "text", text: `\n\nCompacting failed${reason}` },
|
|
2110
|
-
},
|
|
2221
|
+
else if (message.compact_result === "failed") {
|
|
2222
|
+
await compaction.finish(message.session_id, message.uuid, "failed", {
|
|
2223
|
+
...(message.compact_error ? { error: message.compact_error } : {}),
|
|
2111
2224
|
});
|
|
2112
2225
|
}
|
|
2113
2226
|
break;
|
|
2114
2227
|
}
|
|
2115
2228
|
case "compact_boundary": {
|
|
2229
|
+
// This is the only frame carrying the token counts, and it
|
|
2230
|
+
// arrives AFTER the terminal `status` — so it enriches the
|
|
2231
|
+
// lifecycle rather than reporting an outcome again
|
|
2232
|
+
// (`enrichTerminal`): the update omits `status`, leaving exactly
|
|
2233
|
+
// one terminal transition the client can see (R2.2). It also
|
|
2234
|
+
// stands alone when no `status` preceded it (an automatic
|
|
2235
|
+
// compaction, or a replay that dropped the opening frame).
|
|
2236
|
+
const compactMetadata = message.compact_metadata;
|
|
2237
|
+
await compaction.finish(message.session_id, message.uuid, "completed", compactMetadata ? contextCompactionMetadataFromBoundary(compactMetadata) : {}, true);
|
|
2116
2238
|
// Refresh the displayed usage immediately so the client doesn't
|
|
2117
2239
|
// keep showing the stale pre-compaction size (e.g. "944k/1m")
|
|
2118
|
-
// right after the
|
|
2240
|
+
// right after the compaction tool call completes, which is
|
|
2119
2241
|
// confusing and wrong.
|
|
2120
2242
|
//
|
|
2121
2243
|
// Prefer the SDK's authoritative post-compaction `used` via
|
|
@@ -2130,10 +2252,6 @@ export class ClaudeAcpAgent {
|
|
|
2130
2252
|
// `size` keeps coming from session.contextWindowSize —
|
|
2131
2253
|
// compaction frees occupancy, it doesn't change the model's
|
|
2132
2254
|
// window.
|
|
2133
|
-
//
|
|
2134
|
-
// The "Compacting completed." text is emitted from the `status`
|
|
2135
|
-
// handler (keyed on `compact_result`), not here, so the failure
|
|
2136
|
-
// path gets a message too.
|
|
2137
2255
|
const usedTokens = await fetchContextUsedTokens(session.query, this.logger);
|
|
2138
2256
|
lastAssistantUsage = null;
|
|
2139
2257
|
lastAssistantTotalUsage = usedTokens ?? 0;
|
|
@@ -2149,11 +2267,24 @@ export class ClaudeAcpAgent {
|
|
|
2149
2267
|
break;
|
|
2150
2268
|
}
|
|
2151
2269
|
case "local_command_output": {
|
|
2270
|
+
// A failed `/compact` also prints its error here; the tool
|
|
2271
|
+
// lifecycle already carried it, so consume that one duplicate
|
|
2272
|
+
// (matched by text, once) without hiding other command output.
|
|
2273
|
+
if (compaction.consumeDuplicateErrorOutput(message.content)) {
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
// R2.3/R2.4: a `/usage` turn publishes its structured render
|
|
2277
|
+
// here instead; anything else, and any way out, publishes the
|
|
2278
|
+
// CLI's own text byte-for-byte.
|
|
2279
|
+
const usageMarkdown = await takeUsageMarkdown(message.content);
|
|
2280
|
+
if (usageMarkdown === null) {
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2152
2283
|
await sendUpdate({
|
|
2153
2284
|
sessionId: message.session_id,
|
|
2154
2285
|
update: {
|
|
2155
2286
|
sessionUpdate: "agent_message_chunk",
|
|
2156
|
-
content: { type: "text", text: message.content },
|
|
2287
|
+
content: { type: "text", text: usageMarkdown ?? message.content },
|
|
2157
2288
|
},
|
|
2158
2289
|
});
|
|
2159
2290
|
break;
|
|
@@ -2191,6 +2322,10 @@ export class ClaudeAcpAgent {
|
|
|
2191
2322
|
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
2192
2323
|
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
2193
2324
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
2325
|
+
// An interrupt can pre-empt the result entirely, so the
|
|
2326
|
+
// lifecycle's own reset there never ran; close it here or a
|
|
2327
|
+
// half-open compaction would leak into the next turn.
|
|
2328
|
+
compaction.reset();
|
|
2194
2329
|
settleActive({ stopReason: "cancelled", usage: sessionUsage(session) });
|
|
2195
2330
|
// An interrupt can pre-empt the turn's result entirely
|
|
2196
2331
|
// (nothing ran the result-case `finally`), so close the
|
|
@@ -2253,6 +2388,9 @@ export class ClaudeAcpAgent {
|
|
|
2253
2388
|
else if (!session.cancelled &&
|
|
2254
2389
|
session.activeTurn &&
|
|
2255
2390
|
!session.activeTurn.settled) {
|
|
2391
|
+
// Same reason as the cancelled branch: this turn will never
|
|
2392
|
+
// reach the result that would have reset the lifecycle.
|
|
2393
|
+
compaction.reset();
|
|
2256
2394
|
// Deliberately only the ACTIVE turn: a queued turn that
|
|
2257
2395
|
// was never echoed is NOT failed here, because an idle
|
|
2258
2396
|
// can legitimately precede the SDK picking up freshly
|
|
@@ -2668,7 +2806,7 @@ export class ClaudeAcpAgent {
|
|
|
2668
2806
|
// the map in that case).
|
|
2669
2807
|
if (!isAutonomousResult) {
|
|
2670
2808
|
recordResultForOrphanCommands();
|
|
2671
|
-
ensureActiveTurn();
|
|
2809
|
+
ensureActiveTurn(message.user_message_uuid);
|
|
2672
2810
|
// Once the submitted goal command has produced its own result,
|
|
2673
2811
|
// no older runtime update can still precede it in the ordered
|
|
2674
2812
|
// SDK stream. Stop suppressing updates even when this runtime
|
|
@@ -2699,6 +2837,12 @@ export class ClaudeAcpAgent {
|
|
|
2699
2837
|
// through the early break below, which the gated `finally`
|
|
2700
2838
|
// leaves alone).
|
|
2701
2839
|
const deliveredAssistantText = session.emittedAssistantText;
|
|
2840
|
+
// The deleted "Compacting…" banners counted as delivered text
|
|
2841
|
+
// simply by going through sendUpdate; tool calls don't, so a turn
|
|
2842
|
+
// that ONLY compacted (e.g. `/compact`, promoted at its own
|
|
2843
|
+
// result) needs this to keep its result text from being re-emitted
|
|
2844
|
+
// by the issue-#453 fallback below.
|
|
2845
|
+
const deliveredCompactionOutput = compaction.hasDeliveredOutput;
|
|
2702
2846
|
// Every user-turn result terminates a turn (settle, reject, or
|
|
2703
2847
|
// orphan skip) and the SDK follows it with a trailing
|
|
2704
2848
|
// `session_state_changed: idle` — record the debt so the idle
|
|
@@ -2945,9 +3089,19 @@ export class ClaudeAcpAgent {
|
|
|
2945
3089
|
// the fallback there. (Autonomous results never get here —
|
|
2946
3090
|
// they exit at the early break above — so no background
|
|
2947
3091
|
// prose can be injected into the feed.)
|
|
2948
|
-
|
|
2949
|
-
(!deliveredAssistantText &&
|
|
2950
|
-
|
|
3092
|
+
const shouldForwardResult = session.activeTurn?.isLocalOnlyCommand ||
|
|
3093
|
+
(!deliveredAssistantText &&
|
|
3094
|
+
!deliveredCompactionOutput &&
|
|
3095
|
+
(message.usage.output_tokens ?? 0) === 0);
|
|
3096
|
+
if (shouldForwardResult) {
|
|
3097
|
+
// A `/usage` turn whose output arrives only on the result.
|
|
3098
|
+
// Claiming it here also stops the raw text following a
|
|
3099
|
+
// render already published from another message shape.
|
|
3100
|
+
const usageMarkdown = await takeUsageMarkdown(message.result);
|
|
3101
|
+
if (usageMarkdown === null) {
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
for (const notification of toAcpNotifications(usageMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, this.client, this.logger)) {
|
|
2951
3105
|
await sendUpdate(notification);
|
|
2952
3106
|
}
|
|
2953
3107
|
}
|
|
@@ -3022,6 +3176,11 @@ export class ClaudeAcpAgent {
|
|
|
3022
3176
|
finally {
|
|
3023
3177
|
if (!isAutonomousResult) {
|
|
3024
3178
|
session.emittedAssistantText = false;
|
|
3179
|
+
// A result closes this turn's compaction lifecycle. Reset here
|
|
3180
|
+
// rather than at idle: an owed idle from this turn can arrive
|
|
3181
|
+
// after the next turn has already started, and would erase that
|
|
3182
|
+
// turn's compaction state instead of its own.
|
|
3183
|
+
compaction.reset();
|
|
3025
3184
|
// R1.2: a user turn's terminal result is the moment consumption
|
|
3026
3185
|
// actually changed, so refresh the account quota windows here —
|
|
3027
3186
|
// in the `finally`, which is the one point EVERY exit from this
|
|
@@ -3042,6 +3201,17 @@ export class ClaudeAcpAgent {
|
|
|
3042
3201
|
break;
|
|
3043
3202
|
}
|
|
3044
3203
|
case "stream_event": {
|
|
3204
|
+
// Compaction streams as its own block type. The deltas carry the
|
|
3205
|
+
// generated summary, which stays internal to the agent — all the
|
|
3206
|
+
// client gets is one keep-alive on the open tool call, so a long
|
|
3207
|
+
// compaction doesn't look stalled.
|
|
3208
|
+
const isCompactionProgress = (message.event.type === "content_block_start" &&
|
|
3209
|
+
message.event.content_block.type === "compaction") ||
|
|
3210
|
+
(message.event.type === "content_block_delta" &&
|
|
3211
|
+
message.event.delta.type === "compaction_delta");
|
|
3212
|
+
if (isCompactionProgress) {
|
|
3213
|
+
await compaction.heartbeat(message.session_id, message.uuid);
|
|
3214
|
+
}
|
|
3045
3215
|
// `message_start` carries the Anthropic API message id; capture it
|
|
3046
3216
|
// so the streamed chunks that follow (whose delta events don't carry
|
|
3047
3217
|
// it) can all be tagged with the same, replay-stable id.
|
|
@@ -3264,6 +3434,18 @@ export class ClaudeAcpAgent {
|
|
|
3264
3434
|
if (session.cancelled) {
|
|
3265
3435
|
break;
|
|
3266
3436
|
}
|
|
3437
|
+
// Synthetic assistant frames carry the CLI's local-command output.
|
|
3438
|
+
// On resume the SDK can replay a stale frame from an earlier compact
|
|
3439
|
+
// attempt after a later compaction completed. Scope the suppression
|
|
3440
|
+
// to the compaction lifecycle and the synthetic frame itself rather
|
|
3441
|
+
// than to the owning turn: one model turn may compact more than
|
|
3442
|
+
// once, and its real assistant response must still be delivered.
|
|
3443
|
+
if (message.type === "assistant" &&
|
|
3444
|
+
message.parent_tool_use_id === null &&
|
|
3445
|
+
message.message.model === "<synthetic>" &&
|
|
3446
|
+
compaction.hasDeliveredOutput) {
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3267
3449
|
// Snapshot the latest top-level assistant usage and model so the
|
|
3268
3450
|
// next `result` can emit a usage_update tied to the right context
|
|
3269
3451
|
// window. Subagent messages are excluded to keep the snapshot
|
|
@@ -3302,7 +3484,13 @@ export class ClaudeAcpAgent {
|
|
|
3302
3484
|
message.message.content.includes("<local-command-stdout>")) {
|
|
3303
3485
|
const stripped = stripLocalCommandMetadata(message.message.content);
|
|
3304
3486
|
if (typeof stripped === "string") {
|
|
3305
|
-
|
|
3487
|
+
// The usual shape for a real `/usage`: the comment above names
|
|
3488
|
+
// it as one of the commands the CLI wraps in these markers.
|
|
3489
|
+
const usageMarkdown = await takeUsageMarkdown(stripped);
|
|
3490
|
+
if (usageMarkdown === null) {
|
|
3491
|
+
break;
|
|
3492
|
+
}
|
|
3493
|
+
for (const notification of toAcpNotifications(usageMarkdown ?? stripped, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
3306
3494
|
clientCapabilities: this.clientCapabilities,
|
|
3307
3495
|
parentToolUseId: message.parent_tool_use_id,
|
|
3308
3496
|
cwd: session.cwd,
|
|
@@ -3643,6 +3831,11 @@ export class ClaudeAcpAgent {
|
|
|
3643
3831
|
await session.queryRecreateInFlight;
|
|
3644
3832
|
}
|
|
3645
3833
|
session.cancelled = true;
|
|
3834
|
+
// Every turn's structured `/usage` render is abandoned here — the queue
|
|
3835
|
+
// still holds the active turn at this point, so one sweep covers both.
|
|
3836
|
+
for (const turn of session.turnQueue ?? []) {
|
|
3837
|
+
turn.usageMarkdownAbort?.abort();
|
|
3838
|
+
}
|
|
3646
3839
|
session.pendingExitPlanModeInterruption = undefined;
|
|
3647
3840
|
session.pendingExitPlanContextReset = undefined;
|
|
3648
3841
|
// The stream already ended (see closeQueryStream): every in-flight turn was
|
|
@@ -3685,8 +3878,10 @@ export class ClaudeAcpAgent {
|
|
|
3685
3878
|
}
|
|
3686
3879
|
// Each removed queued turn's user message was already pushed to the SDK,
|
|
3687
3880
|
// which processes input FIFO and will still emit a result for it with no
|
|
3688
|
-
//
|
|
3689
|
-
//
|
|
3881
|
+
// user echo to match (0.3.246+ CLIs do stamp results with the
|
|
3882
|
+
// triggering send's user_message_uuid, which ensureActiveTurn uses as
|
|
3883
|
+
// an exact join when present). Track those so the consumer skips them
|
|
3884
|
+
// (see ensureActiveTurn) rather than misattributing them to the head.
|
|
3690
3885
|
// msg_lifecycle_v1 CLIs get per-uuid tracking drained by the command's
|
|
3691
3886
|
// own terminal lifecycle frame — exact under command coalescing, where
|
|
3692
3887
|
// N queued commands fold into ONE turn emitting one result and a plain
|
|
@@ -4611,14 +4806,28 @@ export class ClaudeAcpAgent {
|
|
|
4611
4806
|
// (R1.6).
|
|
4612
4807
|
session.thinkingEnabled ??
|
|
4613
4808
|
effectiveThinkingConfig(undefined, process.env.MAX_THINKING_TOKENS, this.logger) !==
|
|
4614
|
-
undefined
|
|
4809
|
+
undefined, {
|
|
4810
|
+
// The new model decides whether the entry renders at all; the session's
|
|
4811
|
+
// retained intent decides whether it renders selected. A model without
|
|
4812
|
+
// `xhigh` drops the entry, and `validEffort` then falls back for it the
|
|
4813
|
+
// same way it does for any unsupported level.
|
|
4814
|
+
available: isUltracodeAvailable(newModelInfo?.supportsEffort ? (newModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: session.workflowsDisabled }),
|
|
4815
|
+
enabled: session.ultracode,
|
|
4816
|
+
}, {
|
|
4817
|
+
disableWorkflows: session.workflowsDisabled,
|
|
4818
|
+
// Threaded for the same reason Thinking is (R1.7): an option not
|
|
4819
|
+
// passed through this rebuild silently drops from the picker on
|
|
4820
|
+
// every model switch. The account is model-independent, so the row
|
|
4821
|
+
// and its selection survive unchanged.
|
|
4822
|
+
accounts: session.declaredAccounts,
|
|
4823
|
+
currentAccount: session.currentAccount,
|
|
4824
|
+
});
|
|
4615
4825
|
// Sync effort with the SDK if it changed after the model switch
|
|
4616
4826
|
const newEffortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
4617
4827
|
const newEffort = typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
|
|
4618
4828
|
if (newEffort !== currentEffort) {
|
|
4619
|
-
await session.query.applyFlagSettings(
|
|
4620
|
-
|
|
4621
|
-
});
|
|
4829
|
+
await session.query.applyFlagSettings(ultracodeFlagSettings(newEffort));
|
|
4830
|
+
session.ultracode = isUltracodeValue(newEffort);
|
|
4622
4831
|
}
|
|
4623
4832
|
// Emit current_mode_update only after session.modes AND
|
|
4624
4833
|
// session.configOptions have been fully reconciled. This way, a failure
|
|
@@ -4642,12 +4851,34 @@ export class ClaudeAcpAgent {
|
|
|
4642
4851
|
session.currentAgent = value;
|
|
4643
4852
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
4644
4853
|
}
|
|
4854
|
+
else if (configId === ACCOUNT_CONFIG_ID) {
|
|
4855
|
+
// Resolve BEFORE recording anything: an account nobody declared must
|
|
4856
|
+
// leave the session exactly as it was. `setSessionConfigOption`'s shared
|
|
4857
|
+
// validation already refuses a value that is not among the option's
|
|
4858
|
+
// entries; this second check covers the one path that bypasses it (a
|
|
4859
|
+
// client round-tripping `currentValue`) and keeps the refusal here
|
|
4860
|
+
// rather than in a caller (R6.3).
|
|
4861
|
+
const account = accountForValue(session.declaredAccounts ?? [], value);
|
|
4862
|
+
if (!account) {
|
|
4863
|
+
throw new Error(`Invalid value for config option ${configId}: ${value}`);
|
|
4864
|
+
}
|
|
4865
|
+
// No SDK call, and no query recreate: the account is applied through the
|
|
4866
|
+
// per-session `env` at query CREATION (D2/D9 — the SDK already accepts
|
|
4867
|
+
// one, so no Zed patch is involved), and this session's query keeps the
|
|
4868
|
+
// account it was created under (D7).
|
|
4869
|
+
this.selectedAccount = value;
|
|
4870
|
+
session.currentAccount = value;
|
|
4871
|
+
session.accountEnv = envForAccount(account);
|
|
4872
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
4873
|
+
}
|
|
4645
4874
|
else {
|
|
4646
4875
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
4647
4876
|
if (configId === EFFORT_CONFIG_ID) {
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4877
|
+
// Both keys, always. Selecting a plain level after Ultracode must CLEAR
|
|
4878
|
+
// the flag, and only an explicit `null` clears it — see
|
|
4879
|
+
// `ultracodeFlagSettings`.
|
|
4880
|
+
await session.query.applyFlagSettings(ultracodeFlagSettings(value));
|
|
4881
|
+
session.ultracode = isUltracodeValue(value);
|
|
4651
4882
|
}
|
|
4652
4883
|
}
|
|
4653
4884
|
}
|
|
@@ -4806,11 +5037,15 @@ export class ClaudeAcpAgent {
|
|
|
4806
5037
|
// read post-init, so a setter update that landed during the init
|
|
4807
5038
|
// await is already included.
|
|
4808
5039
|
const effortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
4809
|
-
const
|
|
4810
|
-
|
|
4811
|
-
|
|
5040
|
+
const effortValue = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
|
5041
|
+
const { effortLevel, ultracode } = ultracodeFlagSettings(effortValue);
|
|
5042
|
+
// Only the positive case is re-applied: the replacement's flag layer
|
|
5043
|
+
// starts empty, so clearing a key that was never set would be a control
|
|
5044
|
+
// request that says nothing. `ultracode` is sent WITH its level rather
|
|
5045
|
+
// than alone — the SDK requires an xhigh-capable model for it, and a
|
|
5046
|
+
// flag layer carrying one half of the pair is a state no picker names.
|
|
4812
5047
|
if (effortLevel !== null) {
|
|
4813
|
-
await replacement.applyFlagSettings({ effortLevel });
|
|
5048
|
+
await replacement.applyFlagSettings(ultracode === true ? { effortLevel, ultracode: true } : { effortLevel });
|
|
4814
5049
|
}
|
|
4815
5050
|
const supportsFastMode = session.modelInfos.find((m) => m.value === session.models.currentModelId)
|
|
4816
5051
|
?.supportsFastMode ?? false;
|
|
@@ -5234,9 +5469,28 @@ export class ClaudeAcpAgent {
|
|
|
5234
5469
|
env: { ...baseSettings?.env, ...providerEnv },
|
|
5235
5470
|
};
|
|
5236
5471
|
}
|
|
5472
|
+
// The account this session runs under (R6.3): the selector's last choice
|
|
5473
|
+
// when it names a declared account, else the first declared one. Read from
|
|
5474
|
+
// the resolved settings, because accounts are DECLARED, never discovered
|
|
5475
|
+
// (D4) — nothing here scans a home directory for credential stores. With
|
|
5476
|
+
// none declared there is no overlay and the process's own configuration
|
|
5477
|
+
// home stands, which is exactly the behavior that predates this option.
|
|
5478
|
+
const declaredAccounts = readDeclaredAccounts(settingsManager.getSettings(), this.logger);
|
|
5479
|
+
const accountEntry = accountInForce(declaredAccounts, this.selectedAccount);
|
|
5480
|
+
// The SESSION's account home, never the ADAPTER's (D3): this overlay is
|
|
5481
|
+
// handed to the SDK per session and to nothing else. `process.env` is not
|
|
5482
|
+
// mutated — `CLAUDE_CONFIG_DIR` is read once into a module-level constant
|
|
5483
|
+
// that `settings.ts` uses to find the adapter's OWN settings.json, so a
|
|
5484
|
+
// mutation would be seen by some readers and not others.
|
|
5485
|
+
const accountEnv = accountEntry ? envForAccount(accountEntry.account) : undefined;
|
|
5237
5486
|
const env = {
|
|
5238
5487
|
...process.env,
|
|
5239
5488
|
...userProvidedOptions?.env,
|
|
5489
|
+
// After the client-supplied env: the selector is a live user choice,
|
|
5490
|
+
// and an agent entry that pinned CLAUDE_CONFIG_DIR is precisely the
|
|
5491
|
+
// "account as a second agent" shape this option replaces (D1). Before
|
|
5492
|
+
// the provider routing, which sets ANTHROPIC_* and never this key.
|
|
5493
|
+
...accountEnv,
|
|
5240
5494
|
// Client-managed LLM routing: `providers/set` config wins, else the
|
|
5241
5495
|
// legacy gateway auth request. Routing is baked into the query at
|
|
5242
5496
|
// creation; provider updates recreate loaded queries between turns.
|
|
@@ -5500,6 +5754,7 @@ export class ClaudeAcpAgent {
|
|
|
5500
5754
|
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
|
5501
5755
|
disabledReason: fastModeDisabledReason,
|
|
5502
5756
|
};
|
|
5757
|
+
const workflowsDisabled = settingsManager.getSettings().disableWorkflows === true;
|
|
5503
5758
|
const configOptions = buildConfigOptions(modes, models,
|
|
5504
5759
|
// Catalog-based (see `modelInfos` above), matching the model-switch
|
|
5505
5760
|
// rebuild (which passes `session.modelInfos`): `buildConfigOptions`
|
|
@@ -5513,15 +5768,31 @@ export class ClaudeAcpAgent {
|
|
|
5513
5768
|
// effectiveThinkingConfig(undefined, MAX_THINKING_TOKENS) from above, so
|
|
5514
5769
|
// reusing it avoids re-parsing (and re-logging an invalid) env var —
|
|
5515
5770
|
// exactly one error log per query creation.
|
|
5516
|
-
thinking !== undefined
|
|
5771
|
+
thinking !== undefined, {
|
|
5772
|
+
// Seeded from settings, not inferred from the level: `Settings.ultracode`
|
|
5773
|
+
// is what the user (or a `--settings` payload) actually asked for, while
|
|
5774
|
+
// `xhigh` on its own is a legitimate selection that is not Ultracode.
|
|
5775
|
+
available: isUltracodeAvailable(currentModelInfo?.supportsEffort ? (currentModelInfo.supportedEffortLevels ?? []) : [], { disableWorkflows: workflowsDisabled }),
|
|
5776
|
+
enabled: settingsManager.getSettings().ultracode === true,
|
|
5777
|
+
}, {
|
|
5778
|
+
disableWorkflows: workflowsDisabled,
|
|
5779
|
+
// Both halves of the selector, resolved above alongside the env they
|
|
5780
|
+
// produced — so the row the client renders and the configuration
|
|
5781
|
+
// directory the query was actually started with cannot disagree.
|
|
5782
|
+
accounts: declaredAccounts,
|
|
5783
|
+
currentAccount: accountEntry?.value,
|
|
5784
|
+
});
|
|
5517
5785
|
// Apply the initial effort level to the SDK so it matches the UI default
|
|
5518
5786
|
const initialEffort = configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5787
|
+
// The picker's resolved value, not the raw setting: `buildConfigOptions`
|
|
5788
|
+
// has already dropped Ultracode if this model cannot reach `xhigh`, and the
|
|
5789
|
+
// session must track what is displayed rather than what was requested.
|
|
5790
|
+
const initialUltracode = typeof initialEffort?.currentValue === "string" &&
|
|
5791
|
+
isUltracodeValue(initialEffort.currentValue);
|
|
5519
5792
|
if (initialEffort &&
|
|
5520
5793
|
typeof initialEffort.currentValue === "string" &&
|
|
5521
5794
|
initialEffort.currentValue !== "default") {
|
|
5522
|
-
await q.applyFlagSettings(
|
|
5523
|
-
effortLevel: toSdkEffortLevel(initialEffort.currentValue),
|
|
5524
|
-
});
|
|
5795
|
+
await q.applyFlagSettings(ultracodeFlagSettings(initialEffort.currentValue));
|
|
5525
5796
|
}
|
|
5526
5797
|
// Seed the context window WITHOUT any extra IPC on the session/new path.
|
|
5527
5798
|
// On session/load, the resumed session's own `getContextUsage` report — a
|
|
@@ -5586,6 +5857,11 @@ export class ClaudeAcpAgent {
|
|
|
5586
5857
|
configOptions,
|
|
5587
5858
|
agents,
|
|
5588
5859
|
currentAgent,
|
|
5860
|
+
ultracode: initialUltracode,
|
|
5861
|
+
workflowsDisabled,
|
|
5862
|
+
declaredAccounts,
|
|
5863
|
+
currentAccount: accountEntry?.value,
|
|
5864
|
+
accountEnv,
|
|
5589
5865
|
fastModeEnabled,
|
|
5590
5866
|
fastModeDisabledReason,
|
|
5591
5867
|
abortController,
|
|
@@ -5811,18 +6087,6 @@ function isValidBaseUrl(baseUrl) {
|
|
|
5811
6087
|
}
|
|
5812
6088
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
5813
6089
|
}
|
|
5814
|
-
// Translate a UI effort value into the flag-layer payload. The SDK
|
|
5815
|
-
// shallow-merges `applyFlagSettings`, drops `undefined` during JSON transport,
|
|
5816
|
-
// and only clears a key when an explicit `null` is sent — see
|
|
5817
|
-
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
|
|
5818
|
-
// `"default"` sentinel and `undefined` (effort option absent for the model) to
|
|
5819
|
-
// `null` ensures any previously-applied flag is actually cleared. Typed as
|
|
5820
|
-
// `EffortLevel` (not `Settings["effortLevel"]`): the picker offers whatever
|
|
5821
|
-
// `supportedEffortLevels` reports, which includes the session-scoped `"max"`
|
|
5822
|
-
// that the persisted Settings shape deliberately excludes.
|
|
5823
|
-
function toSdkEffortLevel(value) {
|
|
5824
|
-
return value === undefined || value === "default" ? null : value;
|
|
5825
|
-
}
|
|
5826
6090
|
// `supportedAgents()` always returns Claude Code's built-in subagents — the
|
|
5827
6091
|
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
|
|
5828
6092
|
// has configured none of their own. Those aren't meaningful *main-thread*
|
|
@@ -5962,7 +6226,18 @@ export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel
|
|
|
5962
6226
|
* (`intent ?? (effectiveThinkingConfig(undefined, env, logger) !== undefined)`).
|
|
5963
6227
|
* Both session call sites always supply it (R1.1); `undefined` (direct
|
|
5964
6228
|
* callers/tests) omits the row. */
|
|
5965
|
-
thinkingEnabled
|
|
6229
|
+
thinkingEnabled,
|
|
6230
|
+
/** Display state for the Ultracode entry of the effort picker. Omitted by
|
|
6231
|
+
* direct callers/tests, in which case availability is derived from
|
|
6232
|
+
* `settings` and the entry renders unselected. */
|
|
6233
|
+
ultracode,
|
|
6234
|
+
/** Resolved session settings, read for `disableWorkflows` — the half of the
|
|
6235
|
+
* Ultracode gate that is not a model capability — and, alongside them, the
|
|
6236
|
+
* declared accounts plus the one in force, which decide whether an account
|
|
6237
|
+
* selector is advertised at all (R6.1/R6.2). `accounts` is not a field of
|
|
6238
|
+
* the SDK's `Settings`, so it is carried beside them rather than picked
|
|
6239
|
+
* from them. */
|
|
6240
|
+
settings) {
|
|
5966
6241
|
const options = [
|
|
5967
6242
|
SessionModeManager.configOption(modes),
|
|
5968
6243
|
{
|
|
@@ -5995,6 +6270,11 @@ thinkingEnabled) {
|
|
|
5995
6270
|
? (currentModelInfo.supportedEffortLevels ?? [])
|
|
5996
6271
|
: [];
|
|
5997
6272
|
if (supportedLevels.length > 0) {
|
|
6273
|
+
// Ultracode rides the effort picker as one extra entry past the last level,
|
|
6274
|
+
// which is where the VS Code extension draws it too. It is NOT a level (see
|
|
6275
|
+
// `ultracode.ts`): the entry stands for `xhigh` plus workflow orchestration,
|
|
6276
|
+
// and it is offered only when both halves are reachable.
|
|
6277
|
+
const ultracodeAvailable = ultracode?.available ?? isUltracodeAvailable(supportedLevels, settings);
|
|
5998
6278
|
const effortOptions = [
|
|
5999
6279
|
{ value: "default", name: "Default" },
|
|
6000
6280
|
...supportedLevels.map((level) => ({
|
|
@@ -6004,9 +6284,21 @@ thinkingEnabled) {
|
|
|
6004
6284
|
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
6005
6285
|
.join(" "),
|
|
6006
6286
|
})),
|
|
6287
|
+
...(ultracodeAvailable
|
|
6288
|
+
? [{ value: ULTRACODE_OPTION_VALUE, name: ULTRACODE_OPTION_NAME }]
|
|
6289
|
+
: []),
|
|
6007
6290
|
];
|
|
6008
|
-
const includes = (l) => l === "default" ||
|
|
6009
|
-
|
|
6291
|
+
const includes = (l) => l === "default" ||
|
|
6292
|
+
supportedLevels.includes(l) ||
|
|
6293
|
+
(ultracodeAvailable && l === ULTRACODE_OPTION_VALUE);
|
|
6294
|
+
// `effortOptionValue` resolves the flag against the level BEFORE validation,
|
|
6295
|
+
// so an Ultracode session on a model that dropped `xhigh` falls back to
|
|
6296
|
+
// "Default" through the same path any unsupported level does.
|
|
6297
|
+
const displayedEffort = effortOptionValue(currentEffortLevel, {
|
|
6298
|
+
available: ultracodeAvailable,
|
|
6299
|
+
enabled: ultracode?.enabled ?? false,
|
|
6300
|
+
});
|
|
6301
|
+
const validEffort = displayedEffort && includes(displayedEffort) ? displayedEffort : "default";
|
|
6010
6302
|
options.push({
|
|
6011
6303
|
id: EFFORT_CONFIG_ID,
|
|
6012
6304
|
name: "Effort",
|
|
@@ -6050,6 +6342,15 @@ thinkingEnabled) {
|
|
|
6050
6342
|
],
|
|
6051
6343
|
});
|
|
6052
6344
|
}
|
|
6345
|
+
// The account selector, last because it is the least often touched of the
|
|
6346
|
+
// rows and the only one that governs the NEXT thread rather than this one.
|
|
6347
|
+
// `createAccountConfigOption` returns undefined below two declared accounts,
|
|
6348
|
+
// so the row is genuinely absent there rather than present and empty (D8,
|
|
6349
|
+
// R6.2).
|
|
6350
|
+
const accountOption = createAccountConfigOption(settings?.accounts ?? [], settings?.currentAccount);
|
|
6351
|
+
if (accountOption) {
|
|
6352
|
+
options.push(accountOption);
|
|
6353
|
+
}
|
|
6053
6354
|
return options;
|
|
6054
6355
|
}
|
|
6055
6356
|
// Claude Code CLI persists display strings like "opus[1m]" in settings,
|