@agentclientprotocol/claude-agent-acp 0.77.1-preview.2 → 0.77.1-preview.4
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 +14 -18
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +83 -214
- package/dist/air-extension.d.ts +1 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +1 -0
- package/dist/diff.d.ts +13 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +131 -0
- package/dist/file-change-audit.d.ts +15 -24
- package/dist/file-change-audit.d.ts.map +1 -1
- package/dist/file-change-audit.js +70 -172
- package/dist/tools.d.ts +0 -11
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +0 -73
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -27,9 +27,10 @@ import { decodeClaudePermissionResponse } from "./permissions/response.js";
|
|
|
27
27
|
import { SettingsManager } from "./settings.js";
|
|
28
28
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
29
29
|
import { billsClaudeSubscription, CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE, CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON, claudeLoginRequiredError, claudeSubscriptionNotSupportedError, holdsNonSubscriptionCredential, refuseClaudeSubscriptionTurn, shouldHideClaudeAuth, warnClaudeSubscriptionGuardDegraded, } from "./hide-claude-auth.js";
|
|
30
|
-
import { AGENT_FILE_CHANGE_REPORT_CAPABILITY, agentFileChangeReportMeta,
|
|
30
|
+
import { AGENT_FILE_CHANGE_REPORT_CAPABILITY, agentFileChangeReportMeta, createNativeFileChangeReporter, supportsAgentFileChangeReport, } from "./file-change-audit.js";
|
|
31
31
|
import { ContextCompactionLifecycle, clientSupportsCompactionUpdates, contextCompactionMetadataFromBoundary, isCompactSummaryMessage, } from "./context-compaction.js";
|
|
32
|
-
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse,
|
|
32
|
+
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromToolResult, unregisterHookCallback, } from "./tools.js";
|
|
33
|
+
import { toolUpdateFromDiffToolResponse } from "./diff.js";
|
|
33
34
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
34
35
|
import { acceptedPlanToolResult, ExitPlanCoordinator, executionDiagnostic, exitPlanModeRawOutput, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
35
36
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
@@ -779,13 +780,13 @@ export class ClaudeAcpAgent {
|
|
|
779
780
|
},
|
|
780
781
|
settleCancelledTurn: (original, session, turn) => {
|
|
781
782
|
disarmForceCancel(session);
|
|
782
|
-
|
|
783
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, "cancelled");
|
|
783
784
|
turn.settled = true;
|
|
784
785
|
turn.resolve({ stopReason: "cancelled", usage: sessionUsage(original) });
|
|
785
786
|
},
|
|
786
787
|
settleFailedTurn: (session, turn, error) => {
|
|
787
788
|
disarmForceCancel(session);
|
|
788
|
-
|
|
789
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, "providerError");
|
|
789
790
|
turn.settled = true;
|
|
790
791
|
turn.reject(error);
|
|
791
792
|
},
|
|
@@ -1420,15 +1421,7 @@ export class ClaudeAcpAgent {
|
|
|
1420
1421
|
// user message, so the consumer can't promote the turn from the echo.
|
|
1421
1422
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
1422
1423
|
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
1423
|
-
const
|
|
1424
|
-
? agentFileChangeReportRequestId(params._meta)
|
|
1425
|
-
: undefined;
|
|
1426
|
-
let fileChangeAudit;
|
|
1427
|
-
if (fileChangeReportRequestId &&
|
|
1428
|
-
!session.fileChangeReportRequestIds.has(fileChangeReportRequestId)) {
|
|
1429
|
-
session.fileChangeReportRequestIds.add(fileChangeReportRequestId);
|
|
1430
|
-
fileChangeAudit = createFileChangeAuditTurnState(fileChangeReportRequestId);
|
|
1431
|
-
}
|
|
1424
|
+
const fileChangeReport = session.fileChangeReporter?.request(params._meta);
|
|
1432
1425
|
const isUsageCommand = params.prompt.length === 1 &&
|
|
1433
1426
|
params.prompt[0]?.type === "text" &&
|
|
1434
1427
|
isUsageCommandText(params.prompt[0].text);
|
|
@@ -1443,7 +1436,7 @@ export class ClaudeAcpAgent {
|
|
|
1443
1436
|
isLocalOnlyCommand,
|
|
1444
1437
|
...(isUsageCommand ? { isUsageCommand: true } : {}),
|
|
1445
1438
|
...(usageMarkdownAbort ? { usageMarkdownAbort } : {}),
|
|
1446
|
-
...(
|
|
1439
|
+
...(fileChangeReport ? { fileChangeReport } : {}),
|
|
1447
1440
|
settled: false,
|
|
1448
1441
|
resolve: () => { },
|
|
1449
1442
|
reject: () => { },
|
|
@@ -1803,15 +1796,6 @@ export class ClaudeAcpAgent {
|
|
|
1803
1796
|
throw error;
|
|
1804
1797
|
}
|
|
1805
1798
|
}
|
|
1806
|
-
/** Publish the audit terminal for every turn path that did not reach the
|
|
1807
|
-
* report tool. The support flips the turn state synchronously before its
|
|
1808
|
-
* transport await, so callers can stay fail-open and settle the ACP prompt
|
|
1809
|
-
* immediately without allowing a racing lifecycle path to publish twice. */
|
|
1810
|
-
finishFileChangeAudit(session, turn, reason) {
|
|
1811
|
-
if (!turn.fileChangeAudit || !session.fileChangeAuditSupport)
|
|
1812
|
-
return;
|
|
1813
|
-
void session.fileChangeAuditSupport.finishUnavailable(turn.fileChangeAudit, reason);
|
|
1814
|
-
}
|
|
1815
1799
|
/** Lazily start the per-session consumer that drains the SDK query stream for
|
|
1816
1800
|
* the session's whole life. Idempotent: only the first `prompt()` starts it. */
|
|
1817
1801
|
ensureConsumer(session, sessionId) {
|
|
@@ -1915,16 +1899,6 @@ export class ClaudeAcpAgent {
|
|
|
1915
1899
|
}
|
|
1916
1900
|
return;
|
|
1917
1901
|
}
|
|
1918
|
-
if (isFileChangeAuditReportPhase(session.activeTurn?.fileChangeAudit) &&
|
|
1919
|
-
(update.sessionUpdate === "agent_message_chunk" ||
|
|
1920
|
-
update.sessionUpdate === "agent_thought_chunk" ||
|
|
1921
|
-
update.sessionUpdate === "user_message_chunk" ||
|
|
1922
|
-
update.sessionUpdate === "tool_call" ||
|
|
1923
|
-
update.sessionUpdate === "tool_call_update" ||
|
|
1924
|
-
update.sessionUpdate === "compaction_update" ||
|
|
1925
|
-
update.sessionUpdate === "compaction_summary_chunk")) {
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
1902
|
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1929
1903
|
if (!claudeMeta?.parentToolUseId &&
|
|
1930
1904
|
update.content.type === "text" &&
|
|
@@ -2093,7 +2067,7 @@ export class ClaudeAcpAgent {
|
|
|
2093
2067
|
* a stamp naming an orphaned command consumes the result outright, and a
|
|
2094
2068
|
* stamp naming anything else positively refutes "this is a dead turn's
|
|
2095
2069
|
* result", so the dup-over-loss one-skip must not eat it. */
|
|
2096
|
-
const ensureActiveTurn = (resultUserMessageUuid) => {
|
|
2070
|
+
const ensureActiveTurn = async (resultUserMessageUuid) => {
|
|
2097
2071
|
if (session.activeTurn) {
|
|
2098
2072
|
if (!isHeldOpen(session.activeTurn)) {
|
|
2099
2073
|
return;
|
|
@@ -2115,7 +2089,7 @@ export class ClaudeAcpAgent {
|
|
|
2115
2089
|
// promoted command's own delivery decision is not judged against the
|
|
2116
2090
|
// held turn's followup text (issue #453) — the caller snapshots the
|
|
2117
2091
|
// flag AFTER this runs.
|
|
2118
|
-
settleActive(session.activeTurn.deferredSettle);
|
|
2092
|
+
await settleActive(session.activeTurn.deferredSettle);
|
|
2119
2093
|
}
|
|
2120
2094
|
// Orphan accounting runs BEFORE the head check: an orphan's echo-less
|
|
2121
2095
|
// result can arrive with an EMPTY queue (the common post-cancel
|
|
@@ -2276,10 +2250,10 @@ export class ClaudeAcpAgent {
|
|
|
2276
2250
|
/** Settle the active turn's stored deferred outcome once none of its
|
|
2277
2251
|
* spawned subagents is live. The single drain rule shared by the
|
|
2278
2252
|
* followup-result and idle settle sites, so the two lanes can't drift. */
|
|
2279
|
-
const settleDeferredIfDrained = () => {
|
|
2253
|
+
const settleDeferredIfDrained = async () => {
|
|
2280
2254
|
const turn = session.activeTurn;
|
|
2281
2255
|
if (isHeldOpen(turn) && !turnAwaitingSubagents(turn)) {
|
|
2282
|
-
settleActive(turn.deferredSettle);
|
|
2256
|
+
await settleActive(turn.deferredSettle);
|
|
2283
2257
|
}
|
|
2284
2258
|
};
|
|
2285
2259
|
/** Settle the active turn with `outcome` now — unless subagents it
|
|
@@ -2288,7 +2262,7 @@ export class ClaudeAcpAgent {
|
|
|
2288
2262
|
* turn that can have spawned subagents must route through here: a site
|
|
2289
2263
|
* calling settleActive directly bypasses the hold and re-opens the
|
|
2290
2264
|
* out-of-turn permission deadlock (issue #866) through its lane. */
|
|
2291
|
-
const settleOrDefer = (outcome) => {
|
|
2265
|
+
const settleOrDefer = async (outcome) => {
|
|
2292
2266
|
// No result ends a steered turn: the steer aborted the cycle this result
|
|
2293
2267
|
// may belong to, and the steered one is still to come. Record the outcome
|
|
2294
2268
|
// for the idle lane (see Turn.steeredEchoes); later cycles overwrite it,
|
|
@@ -2303,19 +2277,29 @@ export class ClaudeAcpAgent {
|
|
|
2303
2277
|
session.activeTurn.deferredSettle = outcome;
|
|
2304
2278
|
}
|
|
2305
2279
|
else {
|
|
2306
|
-
settleActive(outcome);
|
|
2280
|
+
await settleActive(outcome);
|
|
2307
2281
|
}
|
|
2308
2282
|
};
|
|
2309
|
-
/**
|
|
2310
|
-
*
|
|
2311
|
-
|
|
2283
|
+
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
2284
|
+
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
2285
|
+
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
2286
|
+
const settleActive = async (result, reportReason = result.stopReason === "cancelled"
|
|
2312
2287
|
? "cancelled"
|
|
2313
2288
|
: "notReported") => {
|
|
2314
2289
|
const turn = session.activeTurn;
|
|
2315
|
-
if (!turn || turn.settled) {
|
|
2290
|
+
if (!turn || turn.settled || turn.settling) {
|
|
2316
2291
|
return;
|
|
2317
2292
|
}
|
|
2318
|
-
|
|
2293
|
+
turn.settling = true;
|
|
2294
|
+
turn.settlingOutcome = result;
|
|
2295
|
+
if (reportReason === "notReported") {
|
|
2296
|
+
await session.fileChangeReporter?.report(turn, session.query);
|
|
2297
|
+
// cancel() can settle a held turn while the bounded checkpoint preview
|
|
2298
|
+
// is in flight. Its cancellation outcome wins; never settle twice.
|
|
2299
|
+
if (turn.settled || session.activeTurn !== turn)
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, reportReason);
|
|
2319
2303
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
2320
2304
|
const wasHeld = isHeldOpen(turn);
|
|
2321
2305
|
turn.settled = true;
|
|
@@ -2350,7 +2334,7 @@ export class ClaudeAcpAgent {
|
|
|
2350
2334
|
this.logger.error(`Session ${params.sessionId}: cannot fail active turn because no unsettled active turn exists: ${error}`);
|
|
2351
2335
|
return;
|
|
2352
2336
|
}
|
|
2353
|
-
|
|
2337
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, "providerError");
|
|
2354
2338
|
turn.settled = true;
|
|
2355
2339
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
2356
2340
|
session.activeTurn = null;
|
|
@@ -2413,7 +2397,7 @@ export class ClaudeAcpAgent {
|
|
|
2413
2397
|
return;
|
|
2414
2398
|
}
|
|
2415
2399
|
sessionFailures.recordActive(failure);
|
|
2416
|
-
settleActive(turnOutcome(session, "end_turn", sessionFailureMeta(failure)), "providerError");
|
|
2400
|
+
await settleActive(turnOutcome(session, "end_turn", sessionFailureMeta(failure)), "providerError");
|
|
2417
2401
|
};
|
|
2418
2402
|
/** Reject every in-flight turn — used when the stream dies. */
|
|
2419
2403
|
const failAllTurns = (error) => {
|
|
@@ -2425,7 +2409,7 @@ export class ClaudeAcpAgent {
|
|
|
2425
2409
|
session.turnQueue = [];
|
|
2426
2410
|
for (const turn of turns) {
|
|
2427
2411
|
if (!turn.settled) {
|
|
2428
|
-
|
|
2412
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, "providerError");
|
|
2429
2413
|
const wasHeld = isHeldOpen(turn);
|
|
2430
2414
|
turn.settled = true;
|
|
2431
2415
|
if (wasHeld) {
|
|
@@ -2512,7 +2496,7 @@ export class ClaudeAcpAgent {
|
|
|
2512
2496
|
this.trackOrphanCommand(session, active.promptUuid, "started");
|
|
2513
2497
|
}
|
|
2514
2498
|
}
|
|
2515
|
-
settleActive(turnOutcome(session, "cancelled"));
|
|
2499
|
+
await settleActive(turnOutcome(session, "cancelled"));
|
|
2516
2500
|
// The cancelled turn's result may never come (that's why the
|
|
2517
2501
|
// backstop fired) — close its delivery stretch here so partial
|
|
2518
2502
|
// streamed text can't suppress the next turn's issue-#453 fallback.
|
|
@@ -2567,7 +2551,7 @@ export class ClaudeAcpAgent {
|
|
|
2567
2551
|
// it), but the stored one is the authoritative source.
|
|
2568
2552
|
const inFlight = session.activeTurn;
|
|
2569
2553
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
2570
|
-
settleActive(session.cancelled
|
|
2554
|
+
await settleActive(session.cancelled
|
|
2571
2555
|
? turnOutcome(session, "cancelled")
|
|
2572
2556
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
2573
2557
|
// Queued turns the SDK never started never ran, so reject them rather
|
|
@@ -2577,7 +2561,7 @@ export class ClaudeAcpAgent {
|
|
|
2577
2561
|
// still here was enqueued afterward and was not part of the cancel.)
|
|
2578
2562
|
for (const queued of [...(session.turnQueue ?? [])]) {
|
|
2579
2563
|
if (!queued.settled) {
|
|
2580
|
-
|
|
2564
|
+
session.fileChangeReporter?.finish(queued.fileChangeReport, "providerError");
|
|
2581
2565
|
queued.settled = true;
|
|
2582
2566
|
queued.reject(RequestError.internalError(undefined, SESSION_ENDED_MESSAGE));
|
|
2583
2567
|
}
|
|
@@ -2849,7 +2833,7 @@ export class ClaudeAcpAgent {
|
|
|
2849
2833
|
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
2850
2834
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
2851
2835
|
await compaction.reset();
|
|
2852
|
-
settleActive(turnOutcome(session, "cancelled"));
|
|
2836
|
+
await settleActive(turnOutcome(session, "cancelled"));
|
|
2853
2837
|
// An interrupt can pre-empt the turn's result entirely
|
|
2854
2838
|
// (nothing ran the result-case `finally`), so close the
|
|
2855
2839
|
// delivery stretch here: idle is the SDK's authoritative
|
|
@@ -2875,7 +2859,7 @@ export class ClaudeAcpAgent {
|
|
|
2875
2859
|
if (session.owedTrailingIdles > 0) {
|
|
2876
2860
|
session.owedTrailingIdles--;
|
|
2877
2861
|
}
|
|
2878
|
-
settleDeferredIfDrained();
|
|
2862
|
+
await settleDeferredIfDrained();
|
|
2879
2863
|
}
|
|
2880
2864
|
else if (session.owedTrailingIdles > 0) {
|
|
2881
2865
|
// Absorb a settled turn's trailing idle. Also covers a
|
|
@@ -2907,7 +2891,7 @@ export class ClaudeAcpAgent {
|
|
|
2907
2891
|
steered.deferredSettle = steered.steeredSettle;
|
|
2908
2892
|
steered.steeredEchoes = undefined;
|
|
2909
2893
|
steered.steeredSettle = undefined;
|
|
2910
|
-
settleDeferredIfDrained();
|
|
2894
|
+
await settleDeferredIfDrained();
|
|
2911
2895
|
}
|
|
2912
2896
|
}
|
|
2913
2897
|
else if (!session.cancelled &&
|
|
@@ -3390,7 +3374,7 @@ export class ClaudeAcpAgent {
|
|
|
3390
3374
|
// the map in that case).
|
|
3391
3375
|
if (!isAutonomousResult) {
|
|
3392
3376
|
recordResultForOrphanCommands();
|
|
3393
|
-
ensureActiveTurn(message.user_message_uuid);
|
|
3377
|
+
await ensureActiveTurn(message.user_message_uuid);
|
|
3394
3378
|
// Once the submitted goal command has produced its own result,
|
|
3395
3379
|
// no older runtime update can still precede it in the ordered
|
|
3396
3380
|
// SDK stream. Stop suppressing updates even when this runtime
|
|
@@ -3560,7 +3544,7 @@ export class ClaudeAcpAgent {
|
|
|
3560
3544
|
// failActive a live turn (the held one, or the user's next
|
|
3561
3545
|
// prompt) whose own result recorded a different outcome.
|
|
3562
3546
|
if (isAutonomousResult) {
|
|
3563
|
-
settleDeferredIfDrained();
|
|
3547
|
+
await settleDeferredIfDrained();
|
|
3564
3548
|
// With no turn in flight OR QUEUED (also after the settle
|
|
3565
3549
|
// above), the stretch holds only autonomous prose — close
|
|
3566
3550
|
// it, so a replayed next prompt isn't silently suppressed by
|
|
@@ -3635,7 +3619,7 @@ export class ClaudeAcpAgent {
|
|
|
3635
3619
|
return;
|
|
3636
3620
|
}
|
|
3637
3621
|
stopReason = "cancelled";
|
|
3638
|
-
settleOrDefer(turnOutcome(session, "cancelled"));
|
|
3622
|
+
await settleOrDefer(turnOutcome(session, "cancelled"));
|
|
3639
3623
|
break;
|
|
3640
3624
|
}
|
|
3641
3625
|
if (pendingExitPlanModeInterruption) {
|
|
@@ -3667,7 +3651,7 @@ export class ClaudeAcpAgent {
|
|
|
3667
3651
|
// settling it out from under them would strand their output
|
|
3668
3652
|
// and permission requests out-of-turn (issue #866's deadlock,
|
|
3669
3653
|
// through the refusal lane).
|
|
3670
|
-
settleOrDefer(turnOutcome(session, "refusal"));
|
|
3654
|
+
await settleOrDefer(turnOutcome(session, "refusal"));
|
|
3671
3655
|
break;
|
|
3672
3656
|
}
|
|
3673
3657
|
// A priority:'now' steer can make the SDK terminate the
|
|
@@ -3678,7 +3662,7 @@ export class ClaudeAcpAgent {
|
|
|
3678
3662
|
if (message.is_error &&
|
|
3679
3663
|
isSteering(session.activeTurn) &&
|
|
3680
3664
|
session.activeTurn.steeredEchoes.size > 0) {
|
|
3681
|
-
settleOrDefer(turnOutcome(session, "end_turn"));
|
|
3665
|
+
await settleOrDefer(turnOutcome(session, "end_turn"));
|
|
3682
3666
|
break;
|
|
3683
3667
|
}
|
|
3684
3668
|
if (!message.is_error && lastAssistantModel !== null) {
|
|
@@ -3816,7 +3800,7 @@ export class ClaudeAcpAgent {
|
|
|
3816
3800
|
await compaction.reset();
|
|
3817
3801
|
}
|
|
3818
3802
|
if (!session.cancelled) {
|
|
3819
|
-
settleOrDefer(turnOutcome(session, stopReason));
|
|
3803
|
+
await settleOrDefer(turnOutcome(session, stopReason));
|
|
3820
3804
|
}
|
|
3821
3805
|
}
|
|
3822
3806
|
finally {
|
|
@@ -4010,7 +3994,7 @@ export class ClaudeAcpAgent {
|
|
|
4010
3994
|
session.owedTrailingIdles++;
|
|
4011
3995
|
// Before activateTurn resets the accumulator, so the
|
|
4012
3996
|
// usage still belongs to the cancelled turn.
|
|
4013
|
-
settleActive(turnOutcome(session, "cancelled"));
|
|
3997
|
+
await settleActive(turnOutcome(session, "cancelled"));
|
|
4014
3998
|
}
|
|
4015
3999
|
else if (isHeldOpen(session.activeTurn)) {
|
|
4016
4000
|
// A turn held open for its background subagents (see
|
|
@@ -4020,7 +4004,7 @@ export class ClaudeAcpAgent {
|
|
|
4020
4004
|
// subagent, but it must not rewrite the stop reason
|
|
4021
4005
|
// either. Its trailing-idle debt stands and is absorbed
|
|
4022
4006
|
// when the drain idle eventually arrives.
|
|
4023
|
-
settleActive(session.activeTurn.deferredSettle);
|
|
4007
|
+
await settleActive(session.activeTurn.deferredSettle);
|
|
4024
4008
|
}
|
|
4025
4009
|
else if (isSteering(session.activeTurn) &&
|
|
4026
4010
|
session.activeTurn.steeredSettle !== undefined) {
|
|
@@ -4036,10 +4020,10 @@ export class ClaudeAcpAgent {
|
|
|
4036
4020
|
// turn and false-fails it (issue #825). Harmless if it
|
|
4037
4021
|
// never comes: the debt absorbs one future idle.
|
|
4038
4022
|
session.owedTrailingIdles++;
|
|
4039
|
-
settleActive(session.activeTurn.steeredSettle);
|
|
4023
|
+
await settleActive(session.activeTurn.steeredSettle);
|
|
4040
4024
|
}
|
|
4041
4025
|
else {
|
|
4042
|
-
settleActive(turnOutcome(session, "end_turn"));
|
|
4026
|
+
await settleActive(turnOutcome(session, "end_turn"));
|
|
4043
4027
|
}
|
|
4044
4028
|
}
|
|
4045
4029
|
// Unlike the no-result teardown lanes, this hand-off must
|
|
@@ -4512,7 +4496,7 @@ export class ClaudeAcpAgent {
|
|
|
4512
4496
|
if (session.turnQueue) {
|
|
4513
4497
|
for (const turn of session.turnQueue) {
|
|
4514
4498
|
if (turn !== session.activeTurn && !turn.settled) {
|
|
4515
|
-
|
|
4499
|
+
session.fileChangeReporter?.finish(turn.fileChangeReport, "cancelled");
|
|
4516
4500
|
turn.settled = true;
|
|
4517
4501
|
// Deliberately no `usage`: a queued turn never ran, so the session
|
|
4518
4502
|
// accumulator (the active turn's tally) is not its spend.
|
|
@@ -4583,20 +4567,18 @@ export class ClaudeAcpAgent {
|
|
|
4583
4567
|
session.pendingEmptyInterruptionDiagnosticCommands.add(turn.promptUuid);
|
|
4584
4568
|
}
|
|
4585
4569
|
}
|
|
4586
|
-
// A deferred active turn (see Turn.deferredSettle) already has its
|
|
4587
|
-
//
|
|
4588
|
-
//
|
|
4589
|
-
//
|
|
4590
|
-
//
|
|
4591
|
-
//
|
|
4592
|
-
//
|
|
4593
|
-
// trailer debt is absorbed by the idle handler when its idle does come.
|
|
4594
|
-
// The turn's own usage snapshot is reported per the cancelled-usage
|
|
4595
|
-
// contract (issue #844).
|
|
4570
|
+
// A deferred active turn (see Turn.deferredSettle) already has its result
|
|
4571
|
+
// and is only held open for subagents. A settling turn likewise has its
|
|
4572
|
+
// result but is awaiting the bounded checkpoint preview. Settle either one
|
|
4573
|
+
// "cancelled" NOW: the consumer cannot process the interrupt's trailing
|
|
4574
|
+
// idle while blocked in that preview, and a held turn may already be idle.
|
|
4575
|
+
// The captured outcome preserves usage and metadata (issue #844), while
|
|
4576
|
+
// reporter state makes a late checkpoint response harmless.
|
|
4596
4577
|
{
|
|
4597
4578
|
const active = session.activeTurn;
|
|
4598
|
-
|
|
4599
|
-
|
|
4579
|
+
const pendingOutcome = active?.deferredSettle ?? active?.settlingOutcome;
|
|
4580
|
+
if (active && pendingOutcome && (isHeldOpen(active) || active.settling)) {
|
|
4581
|
+
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
4600
4582
|
active.settled = true;
|
|
4601
4583
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
4602
4584
|
// unreachable from here): disarm the backstop — none should be
|
|
@@ -4634,8 +4616,8 @@ export class ClaudeAcpAgent {
|
|
|
4634
4616
|
// from it here.
|
|
4635
4617
|
active.resolve({
|
|
4636
4618
|
stopReason: "cancelled",
|
|
4637
|
-
usage:
|
|
4638
|
-
...(
|
|
4619
|
+
usage: pendingOutcome.usage,
|
|
4620
|
+
...(pendingOutcome._meta ? { _meta: pendingOutcome._meta } : {}),
|
|
4639
4621
|
});
|
|
4640
4622
|
}
|
|
4641
4623
|
}
|
|
@@ -4917,13 +4899,6 @@ export class ClaudeAcpAgent {
|
|
|
4917
4899
|
})
|
|
4918
4900
|
: undefined;
|
|
4919
4901
|
let replayTurnId;
|
|
4920
|
-
// Stop-hook additionalContext is persisted as an internal user message.
|
|
4921
|
-
// Once that marker (or the internal tool itself) appears, suppress the
|
|
4922
|
-
// whole audit exchange until its tool result. This also hides a disobedient
|
|
4923
|
-
// model's separate prose message, while an ordinary next user prompt safely
|
|
4924
|
-
// ends an incomplete audit lane.
|
|
4925
|
-
let replayingFileChangeAudit = false;
|
|
4926
|
-
const replayFileChangeAuditToolUseIds = new Set();
|
|
4927
4902
|
const nativeReplayEnabled = clientSupportsSubagents(this.clientCapabilities);
|
|
4928
4903
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
4929
4904
|
const replayTerminalStates = new Map();
|
|
@@ -5075,57 +5050,6 @@ export class ClaudeAcpAgent {
|
|
|
5075
5050
|
if (content === null)
|
|
5076
5051
|
continue;
|
|
5077
5052
|
}
|
|
5078
|
-
const auditBlocks = Array.isArray(content)
|
|
5079
|
-
? content.filter((block) => typeof block === "object" && block !== null)
|
|
5080
|
-
: [];
|
|
5081
|
-
const hasFileChangeAuditMarker = (typeof content === "string" && containsFileChangeAuditMarker(content)) ||
|
|
5082
|
-
auditBlocks.some((block) => typeof block.text === "string" && containsFileChangeAuditMarker(block.text));
|
|
5083
|
-
const fileChangeAuditToolUseIds = auditBlocks.flatMap((block) => (block.type === "tool_use" ||
|
|
5084
|
-
block.type === "server_tool_use" ||
|
|
5085
|
-
block.type === "mcp_tool_use") &&
|
|
5086
|
-
typeof block.name === "string" &&
|
|
5087
|
-
isFileChangeAuditTool(block.name) &&
|
|
5088
|
-
typeof block.id === "string"
|
|
5089
|
-
? [block.id]
|
|
5090
|
-
: []);
|
|
5091
|
-
const replayMessageRole = message.message
|
|
5092
|
-
?.role;
|
|
5093
|
-
if (hasFileChangeAuditMarker || fileChangeAuditToolUseIds.length > 0) {
|
|
5094
|
-
replayingFileChangeAudit = true;
|
|
5095
|
-
for (const toolUseId of fileChangeAuditToolUseIds) {
|
|
5096
|
-
replayFileChangeAuditToolUseIds.add(toolUseId);
|
|
5097
|
-
}
|
|
5098
|
-
continue;
|
|
5099
|
-
}
|
|
5100
|
-
if (replayingFileChangeAudit) {
|
|
5101
|
-
const toolResultIds = auditBlocks.flatMap((block) => (block.type === "tool_result" || block.type === "mcp_tool_result") &&
|
|
5102
|
-
typeof block.tool_use_id === "string"
|
|
5103
|
-
? [block.tool_use_id]
|
|
5104
|
-
: []);
|
|
5105
|
-
let completedReport = false;
|
|
5106
|
-
for (const toolUseId of toolResultIds) {
|
|
5107
|
-
if (replayFileChangeAuditToolUseIds.delete(toolUseId))
|
|
5108
|
-
completedReport = true;
|
|
5109
|
-
}
|
|
5110
|
-
if (completedReport && replayFileChangeAuditToolUseIds.size === 0) {
|
|
5111
|
-
replayingFileChangeAudit = false;
|
|
5112
|
-
continue;
|
|
5113
|
-
}
|
|
5114
|
-
// A denied attempt to call another tool is still part of the hidden
|
|
5115
|
-
// lane. Its result must not end replay suppression before the report.
|
|
5116
|
-
if (toolResultIds.length > 0) {
|
|
5117
|
-
continue;
|
|
5118
|
-
}
|
|
5119
|
-
// The next real user prompt is already represented by the client and
|
|
5120
|
-
// starts a new turn; do not let a missing audit result hide it or the
|
|
5121
|
-
// rest of the replay.
|
|
5122
|
-
if (replayMessageRole === "user") {
|
|
5123
|
-
replayingFileChangeAudit = false;
|
|
5124
|
-
}
|
|
5125
|
-
else {
|
|
5126
|
-
continue;
|
|
5127
|
-
}
|
|
5128
|
-
}
|
|
5129
5053
|
// Claude persists the retained summary as a user message framed with
|
|
5130
5054
|
// continuation instructions for the model. Clients on the compaction
|
|
5131
5055
|
// contract get it materialized as the completed compaction entity at
|
|
@@ -5308,26 +5232,6 @@ export class ClaudeAcpAgent {
|
|
|
5308
5232
|
message: "Session not found",
|
|
5309
5233
|
};
|
|
5310
5234
|
}
|
|
5311
|
-
const fileChangeAudit = session.activeTurn?.fileChangeAudit;
|
|
5312
|
-
if (isFileChangeAuditReportPhase(fileChangeAudit)) {
|
|
5313
|
-
// The hidden continuation is an audit-only lane: it may submit the
|
|
5314
|
-
// wrapper-owned report, but it must not run another command after the
|
|
5315
|
-
// user-visible answer has already completed.
|
|
5316
|
-
if (isFileChangeAuditTool(toolName) && fileChangeAudit?.phase === "collecting") {
|
|
5317
|
-
return { behavior: "allow", updatedInput: toolInput };
|
|
5318
|
-
}
|
|
5319
|
-
return {
|
|
5320
|
-
behavior: "deny",
|
|
5321
|
-
message: "Only the internal file-change report is allowed during the audit.",
|
|
5322
|
-
};
|
|
5323
|
-
}
|
|
5324
|
-
// The tool is intentionally unusable outside a negotiated audit turn.
|
|
5325
|
-
if (isFileChangeAuditTool(toolName)) {
|
|
5326
|
-
return {
|
|
5327
|
-
behavior: "deny",
|
|
5328
|
-
message: "No file-change report was requested for this turn.",
|
|
5329
|
-
};
|
|
5330
|
-
}
|
|
5331
5235
|
// When the tool call originates inside a subagent, attribute the eagerly
|
|
5332
5236
|
// emitted tool_call (and the permission request itself) to the Agent/Task
|
|
5333
5237
|
// tool call that spawned the subagent, mirroring the streamed subagent
|
|
@@ -6042,18 +5946,17 @@ export class ClaudeAcpAgent {
|
|
|
6042
5946
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
6043
5947
|
// the same Map that the streaming message handler will read from.
|
|
6044
5948
|
const taskState = new Map();
|
|
6045
|
-
// Resolve every workspace root once. The
|
|
6046
|
-
// set for lexical path validation, and the SDK receives it below.
|
|
5949
|
+
// Resolve every workspace root once. The native checkpoint report uses
|
|
5950
|
+
// this same set for lexical path validation, and the SDK receives it below.
|
|
6047
5951
|
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
6048
5952
|
const additionalDirectories = [
|
|
6049
5953
|
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
6050
5954
|
...acpAdditionalDirectories,
|
|
6051
5955
|
];
|
|
6052
|
-
const
|
|
6053
|
-
?
|
|
5956
|
+
const fileChangeReporter = supportsAgentFileChangeReport(this.clientCapabilities)
|
|
5957
|
+
? createNativeFileChangeReporter({
|
|
6054
5958
|
cwd: params.cwd,
|
|
6055
5959
|
additionalDirectories,
|
|
6056
|
-
getActiveState: () => this.sessions[sessionId]?.activeTurn?.fileChangeAudit,
|
|
6057
5960
|
publish: async (result) => {
|
|
6058
5961
|
await this.client.sessionUpdate({
|
|
6059
5962
|
sessionId,
|
|
@@ -6118,6 +6021,10 @@ export class ClaudeAcpAgent {
|
|
|
6118
6021
|
settingSources: ["user", "project", "local"],
|
|
6119
6022
|
...(thinking !== undefined && { thinking }),
|
|
6120
6023
|
...userProvidedOptions,
|
|
6024
|
+
// Claude Code uses the same checkpoint store for /rewind. Enable it only
|
|
6025
|
+
// for clients that negotiated per-turn file-change reports; this avoids
|
|
6026
|
+
// snapshot I/O for every other session.
|
|
6027
|
+
...(fileChangeReporter ? { enableFileCheckpointing: true } : {}),
|
|
6121
6028
|
...(settings && { settings }),
|
|
6122
6029
|
env,
|
|
6123
6030
|
// Override certain fields that must be controlled by ACP
|
|
@@ -6127,9 +6034,6 @@ export class ClaudeAcpAgent {
|
|
|
6127
6034
|
mcpServers: {
|
|
6128
6035
|
...(userProvidedOptions?.mcpServers || {}),
|
|
6129
6036
|
...mcpServers,
|
|
6130
|
-
...(fileChangeAuditSupport
|
|
6131
|
-
? { [FILE_CHANGE_AUDIT_SERVER_NAME]: fileChangeAuditSupport.mcpServer }
|
|
6132
|
-
: {}),
|
|
6133
6037
|
},
|
|
6134
6038
|
allowDangerouslySkipPermissions: allowBypass,
|
|
6135
6039
|
permissionMode: initialPermissionMode,
|
|
@@ -6162,14 +6066,6 @@ export class ClaudeAcpAgent {
|
|
|
6162
6066
|
tools,
|
|
6163
6067
|
hooks: {
|
|
6164
6068
|
...userProvidedOptions?.hooks,
|
|
6165
|
-
...(fileChangeAuditSupport
|
|
6166
|
-
? {
|
|
6167
|
-
PreToolUse: [
|
|
6168
|
-
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
6169
|
-
{ hooks: [fileChangeAuditSupport.preToolUseHook] },
|
|
6170
|
-
],
|
|
6171
|
-
}
|
|
6172
|
-
: {}),
|
|
6173
6069
|
PostToolUse: [
|
|
6174
6070
|
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
6175
6071
|
{
|
|
@@ -6240,14 +6136,6 @@ export class ClaudeAcpAgent {
|
|
|
6240
6136
|
],
|
|
6241
6137
|
},
|
|
6242
6138
|
],
|
|
6243
|
-
...(fileChangeAuditSupport
|
|
6244
|
-
? {
|
|
6245
|
-
Stop: [
|
|
6246
|
-
...(userProvidedOptions?.hooks?.Stop || []),
|
|
6247
|
-
{ hooks: [fileChangeAuditSupport.stopHook] },
|
|
6248
|
-
],
|
|
6249
|
-
}
|
|
6250
|
-
: {}),
|
|
6251
6139
|
TaskCreated: [
|
|
6252
6140
|
...(userProvidedOptions?.hooks?.TaskCreated || []),
|
|
6253
6141
|
{
|
|
@@ -6503,8 +6391,7 @@ export class ClaudeAcpAgent {
|
|
|
6503
6391
|
sessionFailureState: createSessionFailureState(),
|
|
6504
6392
|
claudeSubscriptionGuard,
|
|
6505
6393
|
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
6506
|
-
|
|
6507
|
-
fileChangeAuditSupport,
|
|
6394
|
+
fileChangeReporter,
|
|
6508
6395
|
};
|
|
6509
6396
|
timing.phase("register");
|
|
6510
6397
|
return {
|
|
@@ -7184,7 +7071,7 @@ function isTaskTool(toolName) {
|
|
|
7184
7071
|
* permission-surfaced tool_call for them (see `ensureToolCallEmitted`) must be
|
|
7185
7072
|
* resolved explicitly at tool_result time. */
|
|
7186
7073
|
function shouldEmitToolCall(toolName) {
|
|
7187
|
-
return toolName !== "TodoWrite" && !isTaskTool(toolName)
|
|
7074
|
+
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
7188
7075
|
}
|
|
7189
7076
|
/** Build the Claude Code-specific metadata for a tool call. Bash descriptions
|
|
7190
7077
|
* are kept out of ACP's standard `title`, which clients may use as the shell
|
|
@@ -7354,7 +7241,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7354
7241
|
const registerHooks = options?.registerHooks !== false;
|
|
7355
7242
|
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
7356
7243
|
if (typeof content === "string") {
|
|
7357
|
-
if (content.length === 0
|
|
7244
|
+
if (content.length === 0) {
|
|
7358
7245
|
return [];
|
|
7359
7246
|
}
|
|
7360
7247
|
const update = {
|
|
@@ -7388,13 +7275,6 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7388
7275
|
// Unlike `tool_use_result`, entries carry their own tool_use_id, so batched
|
|
7389
7276
|
// messages need no single-block guard.
|
|
7390
7277
|
const toolResultMeta = parseToolResultMeta(options?.toolResultMeta);
|
|
7391
|
-
// A report-phase assistant message may contain a short text preface and the
|
|
7392
|
-
// internal tool call in the same content array. Hide the whole message, not
|
|
7393
|
-
// only the tool block, so it stays absent on session replay as well as live.
|
|
7394
|
-
const containsFileChangeAuditToolUse = content.some((chunk) => (chunk.type === "tool_use" ||
|
|
7395
|
-
chunk.type === "server_tool_use" ||
|
|
7396
|
-
chunk.type === "mcp_tool_use") &&
|
|
7397
|
-
isFileChangeAuditTool(chunk.name));
|
|
7398
7278
|
const output = [];
|
|
7399
7279
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
7400
7280
|
for (const chunk of content) {
|
|
@@ -7402,9 +7282,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7402
7282
|
switch (chunk.type) {
|
|
7403
7283
|
case "text":
|
|
7404
7284
|
case "text_delta": {
|
|
7405
|
-
if (chunk.text
|
|
7406
|
-
!containsFileChangeAuditToolUse &&
|
|
7407
|
-
!containsFileChangeAuditMarker(chunk.text)) {
|
|
7285
|
+
if (chunk.text) {
|
|
7408
7286
|
update = {
|
|
7409
7287
|
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
7410
7288
|
content: {
|
|
@@ -7416,22 +7294,21 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7416
7294
|
break;
|
|
7417
7295
|
}
|
|
7418
7296
|
case "image":
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
};
|
|
7297
|
+
update = {
|
|
7298
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
7299
|
+
content: {
|
|
7300
|
+
type: "image",
|
|
7301
|
+
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
7302
|
+
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
7303
|
+
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
7304
|
+
},
|
|
7305
|
+
};
|
|
7429
7306
|
break;
|
|
7430
7307
|
case "thinking":
|
|
7431
7308
|
case "thinking_delta": {
|
|
7432
7309
|
// Recent models default `thinking.display` to "omitted", which streams
|
|
7433
7310
|
// signature-only thinking blocks whose text is empty.
|
|
7434
|
-
if (chunk.thinking
|
|
7311
|
+
if (chunk.thinking) {
|
|
7435
7312
|
update = {
|
|
7436
7313
|
sessionUpdate: "agent_thought_chunk",
|
|
7437
7314
|
content: {
|
|
@@ -7447,11 +7324,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7447
7324
|
case "mcp_tool_use": {
|
|
7448
7325
|
const alreadyCached = chunk.id in toolUseCache;
|
|
7449
7326
|
toolUseCache[chunk.id] = chunk;
|
|
7450
|
-
if (
|
|
7451
|
-
// Wrapper-owned audit protocol: never surface or register generic
|
|
7452
|
-
// PostToolUse callbacks for the internal tool.
|
|
7453
|
-
}
|
|
7454
|
-
else if (chunk.name === "TodoWrite") {
|
|
7327
|
+
if (chunk.name === "TodoWrite") {
|
|
7455
7328
|
// @ts-expect-error - sometimes input is empty object or undefined
|
|
7456
7329
|
if (Array.isArray(chunk.input?.todos)) {
|
|
7457
7330
|
update = {
|
|
@@ -7579,10 +7452,6 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7579
7452
|
logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
7580
7453
|
break;
|
|
7581
7454
|
}
|
|
7582
|
-
if (isFileChangeAuditTool(toolUse.name)) {
|
|
7583
|
-
delete toolUseCache[chunk.tool_use_id];
|
|
7584
|
-
break;
|
|
7585
|
-
}
|
|
7586
7455
|
// A permission request may have surfaced a plan-rendered (TodoWrite) or
|
|
7587
7456
|
// suppressed (Task*) tool as a real tool_call so the request referenced
|
|
7588
7457
|
// a tool call the client knows about (see `ensureToolCallEmitted`,
|