@simonepri/refined-antigravity-acp 1.2.1 → 1.2.2
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/src/fixes/cancellation-leak/index.d.ts +11 -4
- package/dist/src/fixes/cancellation-leak/index.d.ts.map +1 -1
- package/dist/src/fixes/cancellation-leak/index.js +86 -48
- package/dist/src/fixes/cancellation-leak/index.js.map +1 -1
- package/dist/src/fixes/index.d.ts +1 -0
- package/dist/src/fixes/index.d.ts.map +1 -1
- package/dist/src/fixes/index.js +3 -0
- package/dist/src/fixes/index.js.map +1 -1
- package/dist/src/fixes/repetitive-tool-loop/index.d.ts +51 -0
- package/dist/src/fixes/repetitive-tool-loop/index.d.ts.map +1 -0
- package/dist/src/fixes/repetitive-tool-loop/index.js +280 -0
- package/dist/src/fixes/repetitive-tool-loop/index.js.map +1 -0
- package/package.json +1 -1
- package/readme.md +1 -0
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
* When a user cancels an in-flight turn via `session/cancel`, upstream `agy_acp_server`
|
|
4
4
|
* leaks internal Go/Python cancellation error strings (`"context canceledThe request was cancelled by the client."`)
|
|
5
5
|
* directly into assistant message stream chunks before terminating the turn.
|
|
6
|
+
* Additionally, if a subsequent prompt is sent immediately after cancellation before upstream has finished
|
|
7
|
+
* unwinding, raw agy fails or crashes with concurrent receive_steps errors.
|
|
6
8
|
*
|
|
7
9
|
* Solution:
|
|
8
|
-
* Intercepts inbound `session/update` chunks matching upstream cancellation error strings
|
|
9
|
-
*
|
|
10
|
+
* Intercepts inbound `session/update` chunks matching upstream cancellation error strings and drops them,
|
|
11
|
+
* preserves terminal `tool_call_update` status notifications so client UIs complete running tools,
|
|
12
|
+
* waits for upstream's natural cancellation response with a safety timeout fallback, and queues any
|
|
13
|
+
* subsequent outbound prompt until the previous cancellation has completely unwound.
|
|
10
14
|
*/
|
|
11
15
|
import { type AcpFix, type AcpStreamMessage } from "../../core/types.js";
|
|
12
16
|
export declare const CANCELLATION_ERROR_REGEX: RegExp;
|
|
13
17
|
export declare const CONCURRENT_RECEIVE_STEPS_REGEX: RegExp;
|
|
14
|
-
export declare const
|
|
18
|
+
export declare const DEFAULT_CANCELLATION_TIMEOUT_MS = 2500;
|
|
15
19
|
export declare function isCancellationText(text: string): boolean;
|
|
16
20
|
export declare function extractMessageChunkText(msg: AcpStreamMessage): string | null;
|
|
17
|
-
export
|
|
21
|
+
export interface CancellationOptions {
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function createCancellationLeakFix(options?: CancellationOptions): AcpFix;
|
|
18
25
|
export declare const cancellationLeakFix: AcpFix;
|
|
19
26
|
export declare const interruptionCleanupFix: AcpFix;
|
|
20
27
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/fixes/cancellation-leak/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/fixes/cancellation-leak/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,gBAAgB,EAItB,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,wBAAwB,QACkD,CAAC;AAExF,eAAO,MAAM,8BAA8B,QACmG,CAAC;AAE/I,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAEpD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASxD;AAiBD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,GAAG,IAAI,CAK5E;AAeD,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6GD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAkD/E;AAED,eAAO,MAAM,mBAAmB,QAA8B,CAAC;AAC/D,eAAO,MAAM,sBAAsB,QAAsB,CAAC"}
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
* When a user cancels an in-flight turn via `session/cancel`, upstream `agy_acp_server`
|
|
4
4
|
* leaks internal Go/Python cancellation error strings (`"context canceledThe request was cancelled by the client."`)
|
|
5
5
|
* directly into assistant message stream chunks before terminating the turn.
|
|
6
|
+
* Additionally, if a subsequent prompt is sent immediately after cancellation before upstream has finished
|
|
7
|
+
* unwinding, raw agy fails or crashes with concurrent receive_steps errors.
|
|
6
8
|
*
|
|
7
9
|
* Solution:
|
|
8
|
-
* Intercepts inbound `session/update` chunks matching upstream cancellation error strings
|
|
9
|
-
*
|
|
10
|
+
* Intercepts inbound `session/update` chunks matching upstream cancellation error strings and drops them,
|
|
11
|
+
* preserves terminal `tool_call_update` status notifications so client UIs complete running tools,
|
|
12
|
+
* waits for upstream's natural cancellation response with a safety timeout fallback, and queues any
|
|
13
|
+
* subsequent outbound prompt until the previous cancellation has completely unwound.
|
|
10
14
|
*/
|
|
11
15
|
import { ACP_METHODS, SESSION_UPDATES, STOP_REASONS, isMethod, } from "../../core/types.js";
|
|
12
|
-
import { extractSessionId
|
|
16
|
+
import { extractSessionId } from "../../core/session-cache.js";
|
|
13
17
|
export const CANCELLATION_ERROR_REGEX = /^(?:context\s+canceled)?\s*The\s+request\s+was\s+cancelled\s+by\s+the\s+client\.?$/i;
|
|
14
18
|
export const CONCURRENT_RECEIVE_STEPS_REGEX = /^Agent connection was lost and could not be re-established:\s*Concurrent receive_steps\(\) calls are not supported on this connection\.?$/i;
|
|
15
|
-
export const
|
|
19
|
+
export const DEFAULT_CANCELLATION_TIMEOUT_MS = 2500;
|
|
16
20
|
export function isCancellationText(text) {
|
|
17
21
|
const trimmed = text.trim();
|
|
18
22
|
if (!trimmed)
|
|
@@ -46,15 +50,20 @@ export function extractMessageChunkText(msg) {
|
|
|
46
50
|
return null;
|
|
47
51
|
return extractContentText(update.content);
|
|
48
52
|
}
|
|
49
|
-
function handleOutboundPrompt(msg, state) {
|
|
53
|
+
async function handleOutboundPrompt(msg, state) {
|
|
50
54
|
const sessionId = extractSessionId(msg);
|
|
51
55
|
const id = msg.id;
|
|
52
|
-
if (sessionId
|
|
53
|
-
state.
|
|
54
|
-
|
|
56
|
+
if (sessionId) {
|
|
57
|
+
const cancelling = state.cancellingSessions.get(sessionId);
|
|
58
|
+
if (cancelling) {
|
|
59
|
+
await cancelling.promise;
|
|
60
|
+
}
|
|
61
|
+
if (id !== undefined && id !== null) {
|
|
62
|
+
state.activePrompts.set(sessionId, id);
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
|
-
function handleOutboundCancel(msg, context, state) {
|
|
66
|
+
function handleOutboundCancel(msg, context, state, timeoutMs) {
|
|
58
67
|
const sessionId = extractSessionId(msg);
|
|
59
68
|
if (!sessionId)
|
|
60
69
|
return;
|
|
@@ -62,58 +71,85 @@ function handleOutboundCancel(msg, context, state) {
|
|
|
62
71
|
if (promptId === undefined)
|
|
63
72
|
return;
|
|
64
73
|
state.activePrompts.delete(sessionId);
|
|
65
|
-
state.
|
|
66
|
-
state.cancelledPromptIds.add(promptId);
|
|
67
|
-
const alreadySettled = context.session &&
|
|
68
|
-
getFixData(context.session, CANCEL_PROMPT_SETTLED_KEY) === promptId;
|
|
69
|
-
if (alreadySettled)
|
|
74
|
+
if (state.cancellingSessions.has(sessionId))
|
|
70
75
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
let resolveFn;
|
|
77
|
+
const promise = new Promise((resolve) => {
|
|
78
|
+
resolveFn = resolve;
|
|
79
|
+
});
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
const current = state.cancellingSessions.get(sessionId);
|
|
82
|
+
if (current && current.promptId === promptId) {
|
|
83
|
+
state.cancellingSessions.delete(sessionId);
|
|
84
|
+
state.suppressedLateResponseIds.add(promptId);
|
|
85
|
+
if (context.session) {
|
|
86
|
+
context.session.needsRecycle = true;
|
|
87
|
+
}
|
|
88
|
+
const cancelResponse = {
|
|
89
|
+
jsonrpc: "2.0",
|
|
90
|
+
id: promptId,
|
|
91
|
+
result: { stopReason: STOP_REASONS.CANCELLED },
|
|
92
|
+
};
|
|
93
|
+
context.forwardInbound?.(cancelResponse);
|
|
94
|
+
resolveFn();
|
|
95
|
+
}
|
|
96
|
+
}, timeoutMs);
|
|
97
|
+
timer.unref?.();
|
|
98
|
+
state.cancellingSessions.set(sessionId, {
|
|
99
|
+
promptId,
|
|
100
|
+
timer,
|
|
101
|
+
promise,
|
|
102
|
+
resolve: resolveFn,
|
|
103
|
+
});
|
|
88
104
|
}
|
|
89
|
-
function handleInboundPromptId(msg,
|
|
105
|
+
function handleInboundPromptId(msg, _context, state) {
|
|
90
106
|
const id = msg.id;
|
|
91
107
|
if (id === null || id === undefined)
|
|
92
108
|
return false;
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
if (state.suppressedLateResponseIds.has(id)) {
|
|
110
|
+
state.suppressedLateResponseIds.delete(id);
|
|
95
111
|
return true;
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
for (const [sessId, cancelling] of state.cancellingSessions.entries()) {
|
|
114
|
+
if (cancelling.promptId === id) {
|
|
115
|
+
clearTimeout(cancelling.timer);
|
|
116
|
+
state.cancellingSessions.delete(sessId);
|
|
117
|
+
cancelling.resolve();
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const [sessId, promptId] of state.activePrompts.entries()) {
|
|
122
|
+
if (promptId === id) {
|
|
123
|
+
state.activePrompts.delete(sessId);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
return false;
|
|
101
128
|
}
|
|
102
|
-
|
|
129
|
+
function isSuppressedDuringCancellation(msg, sessionId, state) {
|
|
130
|
+
if (!sessionId || !state.cancellingSessions.has(sessionId))
|
|
131
|
+
return false;
|
|
132
|
+
if (!isMethod(msg, ACP_METHODS.SESSION_UPDATE))
|
|
133
|
+
return false;
|
|
134
|
+
const update = msg.params?.update;
|
|
135
|
+
return update?.sessionUpdate !== SESSION_UPDATES.TOOL_CALL_UPDATE;
|
|
136
|
+
}
|
|
137
|
+
export function createCancellationLeakFix(options) {
|
|
138
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_CANCELLATION_TIMEOUT_MS;
|
|
103
139
|
const state = {
|
|
104
140
|
activePrompts: new Map(),
|
|
105
|
-
|
|
106
|
-
|
|
141
|
+
cancellingSessions: new Map(),
|
|
142
|
+
suppressedLateResponseIds: new Set(),
|
|
107
143
|
};
|
|
108
144
|
return {
|
|
109
145
|
name: "cancellation-leak",
|
|
110
146
|
description: "Suppresses raw upstream Go/Python cancellation error chunks and ensures prompt settlement during client interruptions",
|
|
111
|
-
onOutbound(msg, context) {
|
|
147
|
+
async onOutbound(msg, context) {
|
|
112
148
|
if (isMethod(msg, ACP_METHODS.SESSION_PROMPT)) {
|
|
113
|
-
handleOutboundPrompt(msg, state);
|
|
149
|
+
await handleOutboundPrompt(msg, state);
|
|
114
150
|
}
|
|
115
151
|
else if (isMethod(msg, ACP_METHODS.SESSION_CANCEL)) {
|
|
116
|
-
handleOutboundCancel(msg, context, state);
|
|
152
|
+
handleOutboundCancel(msg, context, state, timeoutMs);
|
|
117
153
|
}
|
|
118
154
|
return msg;
|
|
119
155
|
},
|
|
@@ -122,10 +158,8 @@ export function createCancellationLeakFix() {
|
|
|
122
158
|
return [];
|
|
123
159
|
}
|
|
124
160
|
const sessionId = extractSessionId(msg) ?? context.session?.sessionId;
|
|
125
|
-
if (sessionId
|
|
126
|
-
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
161
|
+
if (isSuppressedDuringCancellation(msg, sessionId, state)) {
|
|
162
|
+
return [];
|
|
129
163
|
}
|
|
130
164
|
const text = extractMessageChunkText(msg);
|
|
131
165
|
if (text && isCancellationText(text)) {
|
|
@@ -134,9 +168,13 @@ export function createCancellationLeakFix() {
|
|
|
134
168
|
return [msg];
|
|
135
169
|
},
|
|
136
170
|
dispose() {
|
|
171
|
+
for (const cancelling of state.cancellingSessions.values()) {
|
|
172
|
+
clearTimeout(cancelling.timer);
|
|
173
|
+
cancelling.resolve();
|
|
174
|
+
}
|
|
137
175
|
state.activePrompts.clear();
|
|
138
|
-
state.
|
|
139
|
-
state.
|
|
176
|
+
state.cancellingSessions.clear();
|
|
177
|
+
state.suppressedLateResponseIds.clear();
|
|
140
178
|
},
|
|
141
179
|
};
|
|
142
180
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/fixes/cancellation-leak/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/fixes/cancellation-leak/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ export * from "./missing-usage-metrics/index.js";
|
|
|
17
17
|
export * from "./dangling-tool-calls/index.js";
|
|
18
18
|
export * from "./missing-question-fallback/index.js";
|
|
19
19
|
export * from "./premature-turn-stop/index.js";
|
|
20
|
+
export * from "./repetitive-tool-loop/index.js";
|
|
20
21
|
/**
|
|
21
22
|
* Returns a list of all active fixes addressing the upstream issues documented in readme.md.
|
|
22
23
|
* As upstream releases fix these defects, retirement is as simple as removing the fix here
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fixes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fixes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAqB/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,0BAA0B,CAAC;AACzC,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oCAAoC,CAAC;AACnD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAsB7C"}
|
package/dist/src/fixes/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { missingUsageMetricsFix } from "./missing-usage-metrics/index.js";
|
|
|
16
16
|
import { danglingToolCallsFix } from "./dangling-tool-calls/index.js";
|
|
17
17
|
import { missingQuestionFallbackFix } from "./missing-question-fallback/index.js";
|
|
18
18
|
import { prematureTurnStopFix } from "./premature-turn-stop/index.js";
|
|
19
|
+
import { repetitiveToolLoopFix } from "./repetitive-tool-loop/index.js";
|
|
19
20
|
export * from "./missing-localharness/index.js";
|
|
20
21
|
export * from "./missing-system-prompt/index.js";
|
|
21
22
|
export * from "./missing-slash-skills/index.js";
|
|
@@ -34,6 +35,7 @@ export * from "./missing-usage-metrics/index.js";
|
|
|
34
35
|
export * from "./dangling-tool-calls/index.js";
|
|
35
36
|
export * from "./missing-question-fallback/index.js";
|
|
36
37
|
export * from "./premature-turn-stop/index.js";
|
|
38
|
+
export * from "./repetitive-tool-loop/index.js";
|
|
37
39
|
/**
|
|
38
40
|
* Returns a list of all active fixes addressing the upstream issues documented in readme.md.
|
|
39
41
|
* As upstream releases fix these defects, retirement is as simple as removing the fix here
|
|
@@ -49,6 +51,7 @@ export function createDefaultFixes() {
|
|
|
49
51
|
malformedStreamSyntaxFix,
|
|
50
52
|
activeTurnCollisionFix,
|
|
51
53
|
cancellationLeakFix,
|
|
54
|
+
repetitiveToolLoopFix,
|
|
52
55
|
danglingToolCallsFix,
|
|
53
56
|
staleMcpEndpointsFix,
|
|
54
57
|
orphanedCheckpointsFix,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/fixes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/fixes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAExE,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,0BAA0B,CAAC;AACzC,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oCAAoC,CAAC;AACnD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAEhD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO;QACL,sBAAsB;QACtB,sBAAsB;QACtB,qBAAqB;QACrB,eAAe;QACf,wBAAwB;QACxB,wBAAwB;QACxB,sBAAsB;QACtB,mBAAmB;QACnB,qBAAqB;QACrB,oBAAoB;QACpB,oBAAoB;QACpB,sBAAsB;QACtB,uBAAuB;QACvB,kBAAkB;QAClB,wBAAwB;QACxB,sBAAsB;QACtB,sBAAsB;QACtB,0BAA0B;QAC1B,oBAAoB;KACrB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Problem:
|
|
3
|
+
* Upstream `agy_acp_server` does not detect repetitive tool calling loops or autoregressive attractor cycles.
|
|
4
|
+
* When a model degenerates into repeating the same tool call or cycling through the same set of file slices,
|
|
5
|
+
* raw agy blindly executes tool calls indefinitely, burning thousands of tokens and freezing editor sessions.
|
|
6
|
+
*
|
|
7
|
+
* Solution:
|
|
8
|
+
* Tracks canonicalized tool call signatures per prompt turn using suffix cycle matching.
|
|
9
|
+
* When identical tool calls or periodic cycles repeat beyond safety thresholds, the wrapper sends
|
|
10
|
+
* `session/cancel` upstream to halt runaway execution, auto-completes pending tool updates, emits an
|
|
11
|
+
* explanatory message chunk to the editor chat, and cleanly settles the turn.
|
|
12
|
+
*/
|
|
13
|
+
import { type AcpFix } from "../../core/types.js";
|
|
14
|
+
export declare const IGNORED_METADATA_KEYS: Set<string>;
|
|
15
|
+
export declare const READ_ONLY_TOOLS: Set<string>;
|
|
16
|
+
export interface RepetitiveToolLoopOptions {
|
|
17
|
+
maxPeriod?: number;
|
|
18
|
+
readOnlySingleThreshold?: number;
|
|
19
|
+
mutatingSingleThreshold?: number;
|
|
20
|
+
cycleThreshold?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function normalizeKey(key: string): string;
|
|
23
|
+
export declare function canonicalizeValue(val: unknown): unknown;
|
|
24
|
+
export declare function inferToolNameFromUpdate(title?: string | null, kind?: string | null, rawInput?: unknown): string;
|
|
25
|
+
export declare function computeToolCallSignature(toolName: string, rawInput: unknown): string;
|
|
26
|
+
export interface CycleDetectionResult {
|
|
27
|
+
isLoop: boolean;
|
|
28
|
+
cycleLength: number;
|
|
29
|
+
repetitions: number;
|
|
30
|
+
pattern: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare function detectCycle(signatures: readonly string[], options?: {
|
|
33
|
+
maxPeriod?: number;
|
|
34
|
+
singleToolThreshold?: number;
|
|
35
|
+
cycleThreshold?: number;
|
|
36
|
+
}): CycleDetectionResult;
|
|
37
|
+
export declare class RepetitiveToolLoopTracker {
|
|
38
|
+
private readonly sessions;
|
|
39
|
+
private readonly promptIdToSessionId;
|
|
40
|
+
private getOrCreate;
|
|
41
|
+
startTurn(sessionId: string, promptId?: string | number): void;
|
|
42
|
+
getSessionIdForPrompt(promptId: string | number): string | undefined;
|
|
43
|
+
recordToolCall(sessionId: string, toolName: string, rawInput: unknown, options?: RepetitiveToolLoopOptions): CycleDetectionResult;
|
|
44
|
+
isLoopInterrupted(sessionId: string): boolean;
|
|
45
|
+
getActivePromptId(sessionId: string): string | number | undefined;
|
|
46
|
+
clearSession(sessionId: string): void;
|
|
47
|
+
clear(): void;
|
|
48
|
+
}
|
|
49
|
+
export declare function createRepetitiveToolLoopFix(options?: RepetitiveToolLoopOptions): AcpFix;
|
|
50
|
+
export declare const repetitiveToolLoopFix: AcpFix;
|
|
51
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/fixes/repetitive-tool-loop/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAKL,KAAK,MAAM,EAKZ,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,qBAAqB,aAAwD,CAAC;AAE3F,eAAO,MAAM,eAAe,aAO1B,CAAC;AAEH,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAcvD;AAED,wBAAgB,uBAAuB,CACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EACpB,QAAQ,CAAC,EAAE,OAAO,GACjB,MAAM,CAcR;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAWpF;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAkBD,wBAAgB,WAAW,CACzB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;CACpB,GACL,oBAAoB,CA2BtB;AAQD,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsC;IAE1E,OAAO,CAAC,WAAW;IASnB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAU9D,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpE,cAAc,CACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EACjB,OAAO,GAAE,yBAA8B,GACtC,oBAAoB;IA2BvB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI7C,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjE,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrC,KAAK,IAAI,IAAI;CAId;AA2GD,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,yBAA8B,GAAG,MAAM,CAmC3F;AAED,eAAO,MAAM,qBAAqB,QAAgC,CAAC"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Problem:
|
|
3
|
+
* Upstream `agy_acp_server` does not detect repetitive tool calling loops or autoregressive attractor cycles.
|
|
4
|
+
* When a model degenerates into repeating the same tool call or cycling through the same set of file slices,
|
|
5
|
+
* raw agy blindly executes tool calls indefinitely, burning thousands of tokens and freezing editor sessions.
|
|
6
|
+
*
|
|
7
|
+
* Solution:
|
|
8
|
+
* Tracks canonicalized tool call signatures per prompt turn using suffix cycle matching.
|
|
9
|
+
* When identical tool calls or periodic cycles repeat beyond safety thresholds, the wrapper sends
|
|
10
|
+
* `session/cancel` upstream to halt runaway execution, auto-completes pending tool updates, emits an
|
|
11
|
+
* explanatory message chunk to the editor chat, and cleanly settles the turn.
|
|
12
|
+
*/
|
|
13
|
+
import { ACP_METHODS, SESSION_UPDATES, STOP_REASONS, isMethod, } from "../../core/types.js";
|
|
14
|
+
import { extractSessionId } from "../../core/session-cache.js";
|
|
15
|
+
export const IGNORED_METADATA_KEYS = new Set(["toolaction", "toolsummary", "description"]);
|
|
16
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
17
|
+
"view_file",
|
|
18
|
+
"list_directory",
|
|
19
|
+
"find_file",
|
|
20
|
+
"read_url_content",
|
|
21
|
+
"search_web",
|
|
22
|
+
"read_file",
|
|
23
|
+
]);
|
|
24
|
+
export function normalizeKey(key) {
|
|
25
|
+
return key.toLowerCase().replace(/_/g, "");
|
|
26
|
+
}
|
|
27
|
+
export function canonicalizeValue(val) {
|
|
28
|
+
if (val === null || val === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof val !== "object")
|
|
31
|
+
return val;
|
|
32
|
+
if (Array.isArray(val)) {
|
|
33
|
+
return val.map(canonicalizeValue);
|
|
34
|
+
}
|
|
35
|
+
const sortedObj = {};
|
|
36
|
+
const entries = Object.entries(val);
|
|
37
|
+
const filteredEntries = entries.filter(([k]) => !IGNORED_METADATA_KEYS.has(normalizeKey(k)));
|
|
38
|
+
filteredEntries.sort(([a], [b]) => normalizeKey(a).localeCompare(normalizeKey(b)));
|
|
39
|
+
for (const [k, v] of filteredEntries) {
|
|
40
|
+
sortedObj[normalizeKey(k)] = canonicalizeValue(v);
|
|
41
|
+
}
|
|
42
|
+
return sortedObj;
|
|
43
|
+
}
|
|
44
|
+
export function inferToolNameFromUpdate(title, kind, rawInput) {
|
|
45
|
+
if (title) {
|
|
46
|
+
const cleaned = title.replace(/^Running:?\s+/i, "").trim();
|
|
47
|
+
if (cleaned.length > 0)
|
|
48
|
+
return cleaned;
|
|
49
|
+
}
|
|
50
|
+
if (kind && kind !== "other") {
|
|
51
|
+
return kind;
|
|
52
|
+
}
|
|
53
|
+
if (rawInput && typeof rawInput === "object") {
|
|
54
|
+
if ("CommandLine" in rawInput)
|
|
55
|
+
return "run_command";
|
|
56
|
+
if ("AbsolutePath" in rawInput)
|
|
57
|
+
return "view_file";
|
|
58
|
+
if ("Subagents" in rawInput)
|
|
59
|
+
return "invoke_subagent";
|
|
60
|
+
}
|
|
61
|
+
return "tool";
|
|
62
|
+
}
|
|
63
|
+
export function computeToolCallSignature(toolName, rawInput) {
|
|
64
|
+
let parsedInput = rawInput;
|
|
65
|
+
if (typeof rawInput === "string") {
|
|
66
|
+
try {
|
|
67
|
+
parsedInput = JSON.parse(rawInput);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
parsedInput = rawInput;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const canonical = canonicalizeValue(parsedInput);
|
|
74
|
+
return `${toolName}:${JSON.stringify(canonical)}`;
|
|
75
|
+
}
|
|
76
|
+
function isSuffixCycleMatch(signatures, n, k, repetitions) {
|
|
77
|
+
for (let r = 1; r < repetitions; r++) {
|
|
78
|
+
for (let i = 0; i < k; i++) {
|
|
79
|
+
if (signatures[n - k + i] !== signatures[n - (r + 1) * k + i]) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
export function detectCycle(signatures, options = {}) {
|
|
87
|
+
const maxPeriod = options.maxPeriod ?? 4;
|
|
88
|
+
const singleToolThreshold = options.singleToolThreshold ?? 3;
|
|
89
|
+
const cycleThreshold = options.cycleThreshold ?? 3;
|
|
90
|
+
const n = signatures.length;
|
|
91
|
+
for (let k = 1; k <= maxPeriod; k++) {
|
|
92
|
+
const requiredRepetitions = k === 1 ? singleToolThreshold : cycleThreshold;
|
|
93
|
+
const requiredLength = k * requiredRepetitions;
|
|
94
|
+
if (n < requiredLength)
|
|
95
|
+
continue;
|
|
96
|
+
if (isSuffixCycleMatch(signatures, n, k, requiredRepetitions)) {
|
|
97
|
+
return {
|
|
98
|
+
isLoop: true,
|
|
99
|
+
cycleLength: k,
|
|
100
|
+
repetitions: requiredRepetitions,
|
|
101
|
+
pattern: signatures.slice(n - k),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
isLoop: false,
|
|
107
|
+
cycleLength: 0,
|
|
108
|
+
repetitions: 0,
|
|
109
|
+
pattern: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export class RepetitiveToolLoopTracker {
|
|
113
|
+
sessions = new Map();
|
|
114
|
+
promptIdToSessionId = new Map();
|
|
115
|
+
getOrCreate(sessionId) {
|
|
116
|
+
let state = this.sessions.get(sessionId);
|
|
117
|
+
if (!state) {
|
|
118
|
+
state = { signatures: [], loopInterrupted: false };
|
|
119
|
+
this.sessions.set(sessionId, state);
|
|
120
|
+
}
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
startTurn(sessionId, promptId) {
|
|
124
|
+
const state = this.getOrCreate(sessionId);
|
|
125
|
+
state.activePromptId = promptId;
|
|
126
|
+
state.signatures = [];
|
|
127
|
+
state.loopInterrupted = false;
|
|
128
|
+
if (promptId !== undefined && promptId !== null) {
|
|
129
|
+
this.promptIdToSessionId.set(promptId, sessionId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
getSessionIdForPrompt(promptId) {
|
|
133
|
+
return this.promptIdToSessionId.get(promptId);
|
|
134
|
+
}
|
|
135
|
+
recordToolCall(sessionId, toolName, rawInput, options = {}) {
|
|
136
|
+
const state = this.getOrCreate(sessionId);
|
|
137
|
+
if (state.loopInterrupted) {
|
|
138
|
+
return { isLoop: true, cycleLength: 0, repetitions: 0, pattern: [] };
|
|
139
|
+
}
|
|
140
|
+
const signature = computeToolCallSignature(toolName, rawInput);
|
|
141
|
+
state.signatures.push(signature);
|
|
142
|
+
const isReadOnly = READ_ONLY_TOOLS.has(toolName);
|
|
143
|
+
const singleThreshold = isReadOnly
|
|
144
|
+
? (options.readOnlySingleThreshold ?? 3)
|
|
145
|
+
: (options.mutatingSingleThreshold ?? 5);
|
|
146
|
+
const result = detectCycle(state.signatures, {
|
|
147
|
+
maxPeriod: options.maxPeriod ?? 4,
|
|
148
|
+
singleToolThreshold: singleThreshold,
|
|
149
|
+
cycleThreshold: options.cycleThreshold ?? 3,
|
|
150
|
+
});
|
|
151
|
+
if (result.isLoop) {
|
|
152
|
+
state.loopInterrupted = true;
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
isLoopInterrupted(sessionId) {
|
|
157
|
+
return this.sessions.get(sessionId)?.loopInterrupted ?? false;
|
|
158
|
+
}
|
|
159
|
+
getActivePromptId(sessionId) {
|
|
160
|
+
return this.sessions.get(sessionId)?.activePromptId;
|
|
161
|
+
}
|
|
162
|
+
clearSession(sessionId) {
|
|
163
|
+
this.sessions.delete(sessionId);
|
|
164
|
+
}
|
|
165
|
+
clear() {
|
|
166
|
+
this.sessions.clear();
|
|
167
|
+
this.promptIdToSessionId.clear();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function interruptLoopExecution(sessionId, toolCallId, toolName, loopResult, context) {
|
|
171
|
+
await context
|
|
172
|
+
.writeToChild({
|
|
173
|
+
jsonrpc: "2.0",
|
|
174
|
+
method: ACP_METHODS.SESSION_CANCEL,
|
|
175
|
+
params: { sessionId },
|
|
176
|
+
})
|
|
177
|
+
.catch(() => { });
|
|
178
|
+
context.forwardInbound?.({
|
|
179
|
+
jsonrpc: "2.0",
|
|
180
|
+
method: ACP_METHODS.SESSION_UPDATE,
|
|
181
|
+
params: {
|
|
182
|
+
sessionId,
|
|
183
|
+
update: {
|
|
184
|
+
sessionUpdate: SESSION_UPDATES.TOOL_CALL_UPDATE,
|
|
185
|
+
toolCallId,
|
|
186
|
+
status: "completed",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const cycleDesc = loopResult.cycleLength > 1
|
|
191
|
+
? `a cycle of ${loopResult.cycleLength} alternating tool calls`
|
|
192
|
+
: `identical tool call (${toolName})`;
|
|
193
|
+
context.forwardInbound?.({
|
|
194
|
+
jsonrpc: "2.0",
|
|
195
|
+
method: ACP_METHODS.SESSION_UPDATE,
|
|
196
|
+
params: {
|
|
197
|
+
sessionId,
|
|
198
|
+
update: {
|
|
199
|
+
sessionUpdate: SESSION_UPDATES.AGENT_MESSAGE_CHUNK,
|
|
200
|
+
content: {
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `\n\n⚠️ Interrupted repetitive tool calling loop: detected ${cycleDesc} repeating without progress. Stopping turn to prevent runaway token usage.`,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function settleInterruptedPromptResponse(msg, sessionId, tracker) {
|
|
209
|
+
if (!("result" in msg) || !tracker.isLoopInterrupted(sessionId)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const res = msg.result;
|
|
213
|
+
if (res && (res.stopReason === STOP_REASONS.CANCELLED || !res.stopReason)) {
|
|
214
|
+
tracker.startTurn(sessionId);
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
...msg,
|
|
218
|
+
result: {
|
|
219
|
+
...res,
|
|
220
|
+
stopReason: STOP_REASONS.END_TURN,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function resolveSessionId(msg, tracker) {
|
|
228
|
+
const fromMsg = extractSessionId(msg);
|
|
229
|
+
if (fromMsg)
|
|
230
|
+
return fromMsg;
|
|
231
|
+
const id = msg.id;
|
|
232
|
+
return id !== null && id !== undefined ? tracker.getSessionIdForPrompt(id) : undefined;
|
|
233
|
+
}
|
|
234
|
+
async function handleInboundSessionUpdate(msg, sessionId, tracker, options, context) {
|
|
235
|
+
const update = "params" in msg ? msg.params?.update : undefined;
|
|
236
|
+
if (update?.sessionUpdate !== SESSION_UPDATES.TOOL_CALL) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const toolCallId = update.toolCallId ?? "unknown";
|
|
240
|
+
const toolName = inferToolNameFromUpdate(update.title, update.kind, update.rawInput);
|
|
241
|
+
const loopResult = tracker.recordToolCall(sessionId, toolName, update.rawInput, options);
|
|
242
|
+
if (loopResult.isLoop) {
|
|
243
|
+
await interruptLoopExecution(sessionId, toolCallId, toolName, loopResult, context);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
export function createRepetitiveToolLoopFix(options = {}) {
|
|
249
|
+
const tracker = new RepetitiveToolLoopTracker();
|
|
250
|
+
return {
|
|
251
|
+
name: "repetitive-tool-loop",
|
|
252
|
+
description: "Detects and interrupts repetitive tool calling cycles and autoregressive attractor loops",
|
|
253
|
+
onOutbound(msg, _context) {
|
|
254
|
+
const sessionId = extractSessionId(msg);
|
|
255
|
+
if (!sessionId)
|
|
256
|
+
return msg;
|
|
257
|
+
if (isMethod(msg, ACP_METHODS.SESSION_PROMPT)) {
|
|
258
|
+
const promptId = msg.id;
|
|
259
|
+
tracker.startTurn(sessionId, promptId);
|
|
260
|
+
}
|
|
261
|
+
return msg;
|
|
262
|
+
},
|
|
263
|
+
async onInbound(msg, context) {
|
|
264
|
+
const sessionId = resolveSessionId(msg, tracker);
|
|
265
|
+
if (!sessionId)
|
|
266
|
+
return [msg];
|
|
267
|
+
const settled = settleInterruptedPromptResponse(msg, sessionId, tracker);
|
|
268
|
+
if (settled)
|
|
269
|
+
return settled;
|
|
270
|
+
if (isMethod(msg, ACP_METHODS.SESSION_UPDATE)) {
|
|
271
|
+
const handled = await handleInboundSessionUpdate(msg, sessionId, tracker, options, context);
|
|
272
|
+
if (handled)
|
|
273
|
+
return handled;
|
|
274
|
+
}
|
|
275
|
+
return [msg];
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export const repetitiveToolLoopFix = createRepetitiveToolLoopFix();
|
|
280
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/fixes/repetitive-tool-loop/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -160,6 +160,7 @@ The matrix below documents upstream defects across process startup, turn executi
|
|
|
160
160
|
| [**Dangling Running Tool Calls**](src/fixes/dangling-tool-calls/index.ts) | **Problem**: When commands are backgrounded, upstream never emits terminal `tool_call_update` with `status: "completed"`, leaving editor UIs with an infinite active running animation.<br>🔗 [`problem: dangling running tool call`](src/fixes/dangling-tool-calls/index.test.ts#L67) | Tracks active tool calls and marks backgrounded tools completed immediately, before assistant text streams, and upon turn completion.<br>🔗 [`solution: tool call auto-completion`](src/fixes/dangling-tool-calls/index.test.ts#L79) |
|
|
161
161
|
| [**Interactive Question Deadlock**](src/fixes/missing-question-fallback/index.ts) | **Problem**: When `ask_question` is active, sending a chat prompt or cancel causes upstream to reject the message with _"A foreground turn is already active"_, deadlocking sessions indefinitely.<br>🔗 [`problem: question collision`](src/fixes/missing-question-fallback/index.e2e.test.ts#L16) | Intercepts user chat prompts and cancellations to auto-unblock pending question permission requests to the child process and complete UI tool states.<br>🔗 [`solution: question unblocking`](src/fixes/missing-question-fallback/index.e2e.test.ts#L53) |
|
|
162
162
|
| [**Premature Empty Turn Stop**](src/fixes/premature-turn-stop/index.ts) | **Problem**: Upstream binary prematurely concludes prompt turns with empty completions (`stopReason=16`) without emitting assistant message chunks, leaving editor UIs frozen in silence.<br>🔗 [`problem: premature turn termination`](src/fixes/premature-turn-stop/index.test.ts#L48) | Injects explicit system prompt steering instructing the model to always emit clear status messages and never terminate turns silently.<br>🔗 [`solution: prompt steering`](src/fixes/premature-turn-stop/index.test.ts#L79) |
|
|
163
|
+
| [**Repetitive Tool Calling Loop**](src/fixes/repetitive-tool-loop/index.ts) | **Problem**: Upstream binary has no loop detector and will execute cyclic or identical tool calls indefinitely, burning thousands of tokens and freezing sessions.<br>🔗 [`problem: repetitive tool loop`](src/fixes/repetitive-tool-loop/index.e2e.test.ts#L25) | Tracks tool signatures per turn using suffix cycle matching, sending `session/cancel` upstream, auto-completing tools, and notifying the user when cycles repeat.<br>🔗 [`solution: loop circuit breaker`](src/fixes/repetitive-tool-loop/index.e2e.test.ts#L81) |
|
|
163
164
|
|
|
164
165
|
## Setup
|
|
165
166
|
|