cclawd 1.0.2 → 1.0.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.
Files changed (68) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/plugin-sdk/active-listener-CN-tMEvN.js +35 -0
  3. package/dist/plugin-sdk/api-key-rotation-CimGYMBc.js +176 -0
  4. package/dist/plugin-sdk/audio-preflight-C-xXBoE2.js +51 -0
  5. package/dist/plugin-sdk/audio-transcription-runner-CTIHpebA.js +2173 -0
  6. package/dist/plugin-sdk/audit-membership-runtime-BFatB2LJ.js +58 -0
  7. package/dist/plugin-sdk/channel-activity-DO0FEzyj.js +95 -0
  8. package/dist/plugin-sdk/channel-web-Da-__nUF.js +2238 -0
  9. package/dist/plugin-sdk/commands-registry-6no2NNrY.js +1118 -0
  10. package/dist/plugin-sdk/compact.runtime-CCoclu5e.js +35 -0
  11. package/dist/plugin-sdk/config-B9ODwgpz.js +37426 -0
  12. package/dist/plugin-sdk/deliver-B1fFpKjV.js +1757 -0
  13. package/dist/plugin-sdk/deliver-runtime-DB-VRMe1.js +15 -0
  14. package/dist/plugin-sdk/deps-send-discord.runtime-DklqycYG.js +15 -0
  15. package/dist/plugin-sdk/deps-send-imessage.runtime-Chs8zeon.js +14 -0
  16. package/dist/plugin-sdk/deps-send-signal.runtime-clW9aSJP.js +13 -0
  17. package/dist/plugin-sdk/deps-send-slack.runtime-BUx0LYY1.js +13 -0
  18. package/dist/plugin-sdk/deps-send-telegram.runtime-LECSHgMG.js +16 -0
  19. package/dist/plugin-sdk/deps-send-whatsapp.runtime-D2d65fw0.js +40 -0
  20. package/dist/plugin-sdk/diagnostic-CxIvS-C2.js +315 -0
  21. package/dist/plugin-sdk/dispatch-BqlR4dPx.js +105863 -0
  22. package/dist/plugin-sdk/env-b9k1PHMI.js +34 -0
  23. package/dist/plugin-sdk/fetch-PoxzAANT.js +326 -0
  24. package/dist/plugin-sdk/fetch-guard-4UVSZ0uS.js +164 -0
  25. package/dist/plugin-sdk/image-Ch6M4tnJ.js +2420 -0
  26. package/dist/plugin-sdk/image-runtime-CSh2o5wY.js +8 -0
  27. package/dist/plugin-sdk/index.js +35 -35
  28. package/dist/plugin-sdk/ir-CugsqGH8.js +1312 -0
  29. package/dist/plugin-sdk/local-roots-adnEg9zb.js +217 -0
  30. package/dist/plugin-sdk/logger-D6zRubj0.js +1164 -0
  31. package/dist/plugin-sdk/login-CYvkQ0At.js +54 -0
  32. package/dist/plugin-sdk/login-qr-ll4NtaT5.js +316 -0
  33. package/dist/plugin-sdk/manager-CHy8IclH.js +3959 -0
  34. package/dist/plugin-sdk/manager-runtime-C70EkEr7.js +11 -0
  35. package/dist/plugin-sdk/outbound-Wzs2iN7X.js +216 -0
  36. package/dist/plugin-sdk/outbound-attachment-khXJwucX.js +17 -0
  37. package/dist/plugin-sdk/paths-BtVqCdw4.js +3063 -0
  38. package/dist/plugin-sdk/pi-model-discovery-Dh4ziodY.js +131 -0
  39. package/dist/plugin-sdk/pi-model-discovery-runtime-b83Xe-HT.js +8 -0
  40. package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-C1z5CDBF.js +349 -0
  41. package/dist/plugin-sdk/proxy-fetch-CJEmoBxi.js +54 -0
  42. package/dist/plugin-sdk/pw-ai-Dj3Cvlzl.js +1990 -0
  43. package/dist/plugin-sdk/qmd-manager-egHUAseQ.js +1581 -0
  44. package/dist/plugin-sdk/resolve-outbound-target-BiICvIKs.js +38 -0
  45. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DNApufzW.js +9 -0
  46. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-CBmtfIQ8.js +13 -0
  47. package/dist/plugin-sdk/send-CScblaI4.js +532 -0
  48. package/dist/plugin-sdk/send-CeHhnld6.js +407 -0
  49. package/dist/plugin-sdk/send-DP_c8JfR.js +3277 -0
  50. package/dist/plugin-sdk/send-Dc5fI6e8.js +495 -0
  51. package/dist/plugin-sdk/send-l-77_s1_.js +2507 -0
  52. package/dist/plugin-sdk/session-CkOKZaqa.js +166 -0
  53. package/dist/plugin-sdk/signal.js +2 -2
  54. package/dist/plugin-sdk/skill-commands-BohYCgkq.js +336 -0
  55. package/dist/plugin-sdk/slash-commands.runtime-DpLfVTM6.js +8 -0
  56. package/dist/plugin-sdk/slash-dispatch.runtime-CASMHwpm.js +35 -0
  57. package/dist/plugin-sdk/slash-skill-commands.runtime-D7rrJEci.js +9 -0
  58. package/dist/plugin-sdk/sqlite-CJE3X7Mv.js +1005 -0
  59. package/dist/plugin-sdk/subagent-registry-runtime-B1oo5bih.js +35 -0
  60. package/dist/plugin-sdk/tables-D5VgpTmm.js +53 -0
  61. package/dist/plugin-sdk/target-errors-C6zZ_OpA.js +191 -0
  62. package/dist/plugin-sdk/tokens-DUnJnpMS.js +50 -0
  63. package/dist/plugin-sdk/web-TfUM1nSi.js +39 -0
  64. package/dist/plugin-sdk/whatsapp-actions-DuWJ0j1r.js +71 -0
  65. package/extensions/mfa-auth/index.ts +36 -17
  66. package/extensions/mfa-auth/src/auth-manager.ts +4 -0
  67. package/extensions/mfa-auth/src/notification-service.ts +5 -1
  68. package/package.json +1 -1
@@ -0,0 +1,131 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-DUslC3ob.js";
2
+ import { Rl as ensureAuthProfileStore, Vt as normalizeProviderId } from "./config-B9ODwgpz.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import * as PiCodingAgent from "@mariozechner/pi-coding-agent";
6
+ //#region src/agents/pi-auth-credentials.ts
7
+ function convertAuthProfileCredentialToPi(cred) {
8
+ if (cred.type === "api_key") {
9
+ const key = typeof cred.key === "string" ? cred.key.trim() : "";
10
+ if (!key) return null;
11
+ return {
12
+ type: "api_key",
13
+ key
14
+ };
15
+ }
16
+ if (cred.type === "token") {
17
+ const token = typeof cred.token === "string" ? cred.token.trim() : "";
18
+ if (!token) return null;
19
+ if (typeof cred.expires === "number" && Number.isFinite(cred.expires) && Date.now() >= cred.expires) return null;
20
+ return {
21
+ type: "api_key",
22
+ key: token
23
+ };
24
+ }
25
+ if (cred.type === "oauth") {
26
+ const access = typeof cred.access === "string" ? cred.access.trim() : "";
27
+ const refresh = typeof cred.refresh === "string" ? cred.refresh.trim() : "";
28
+ if (!access || !refresh || !Number.isFinite(cred.expires) || cred.expires <= 0) return null;
29
+ return {
30
+ type: "oauth",
31
+ access,
32
+ refresh,
33
+ expires: cred.expires
34
+ };
35
+ }
36
+ return null;
37
+ }
38
+ function resolvePiCredentialMapFromStore(store) {
39
+ const credentials = {};
40
+ for (const credential of Object.values(store.profiles)) {
41
+ const provider = normalizeProviderId(String(credential.provider ?? "")).trim();
42
+ if (!provider || credentials[provider]) continue;
43
+ const converted = convertAuthProfileCredentialToPi(credential);
44
+ if (converted) credentials[provider] = converted;
45
+ }
46
+ return credentials;
47
+ }
48
+ //#endregion
49
+ //#region src/agents/pi-model-discovery.ts
50
+ var pi_model_discovery_exports = /* @__PURE__ */ __exportAll({
51
+ AuthStorage: () => PiAuthStorageClass,
52
+ ModelRegistry: () => PiModelRegistryClass,
53
+ discoverAuthStorage: () => discoverAuthStorage,
54
+ discoverModels: () => discoverModels
55
+ });
56
+ const PiAuthStorageClass = PiCodingAgent.AuthStorage;
57
+ const PiModelRegistryClass = PiCodingAgent.ModelRegistry;
58
+ function createInMemoryAuthStorageBackend(initialData) {
59
+ let snapshot = JSON.stringify(initialData, null, 2);
60
+ return { withLock(update) {
61
+ const { result, next } = update(snapshot);
62
+ if (typeof next === "string") snapshot = next;
63
+ return result;
64
+ } };
65
+ }
66
+ function isRecord(value) {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }
69
+ function scrubLegacyStaticAuthJsonEntries(pathname) {
70
+ if (process.env.OPENCLAW_AUTH_STORE_READONLY === "1") return;
71
+ if (!fs.existsSync(pathname)) return;
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(fs.readFileSync(pathname, "utf8"));
75
+ } catch {
76
+ return;
77
+ }
78
+ if (!isRecord(parsed)) return;
79
+ let changed = false;
80
+ for (const [provider, value] of Object.entries(parsed)) {
81
+ if (!isRecord(value)) continue;
82
+ if (value.type !== "api_key") continue;
83
+ delete parsed[provider];
84
+ changed = true;
85
+ }
86
+ if (!changed) return;
87
+ if (Object.keys(parsed).length === 0) {
88
+ fs.rmSync(pathname, { force: true });
89
+ return;
90
+ }
91
+ fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
92
+ fs.chmodSync(pathname, 384);
93
+ }
94
+ function createAuthStorage(AuthStorageLike, path, creds) {
95
+ const withInMemory = AuthStorageLike;
96
+ if (typeof withInMemory.inMemory === "function") return withInMemory.inMemory(creds);
97
+ const withFromStorage = AuthStorageLike;
98
+ if (typeof withFromStorage.fromStorage === "function") {
99
+ const backendCtor = PiCodingAgent.InMemoryAuthStorageBackend;
100
+ const backend = typeof backendCtor === "function" ? new backendCtor() : createInMemoryAuthStorageBackend(creds);
101
+ backend.withLock(() => ({
102
+ result: void 0,
103
+ next: JSON.stringify(creds, null, 2)
104
+ }));
105
+ return withFromStorage.fromStorage(backend);
106
+ }
107
+ const withFactory = AuthStorageLike;
108
+ const withRuntimeOverride = typeof withFactory.create === "function" ? withFactory.create(path) : new AuthStorageLike(path);
109
+ if (typeof withRuntimeOverride.setRuntimeApiKey === "function") for (const [provider, credential] of Object.entries(creds)) {
110
+ if (credential.type === "api_key") {
111
+ withRuntimeOverride.setRuntimeApiKey(provider, credential.key);
112
+ continue;
113
+ }
114
+ withRuntimeOverride.setRuntimeApiKey(provider, credential.access);
115
+ }
116
+ return withRuntimeOverride;
117
+ }
118
+ function resolvePiCredentials(agentDir) {
119
+ return resolvePiCredentialMapFromStore(ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }));
120
+ }
121
+ function discoverAuthStorage(agentDir) {
122
+ const credentials = resolvePiCredentials(agentDir);
123
+ const authPath = path.join(agentDir, "auth.json");
124
+ scrubLegacyStaticAuthJsonEntries(authPath);
125
+ return createAuthStorage(PiAuthStorageClass, authPath, credentials);
126
+ }
127
+ function discoverModels(authStorage, agentDir) {
128
+ return new PiModelRegistryClass(authStorage, path.join(agentDir, "models.json"));
129
+ }
130
+ //#endregion
131
+ export { discoverModels as n, pi_model_discovery_exports as r, discoverAuthStorage as t };
@@ -0,0 +1,8 @@
1
+ import "./paths-BtVqCdw4.js";
2
+ import "./config-B9ODwgpz.js";
3
+ import "./paths-eFexkPEh.js";
4
+ import "./github-copilot-token-Cxf8QYZb.js";
5
+ import "./logger-D6zRubj0.js";
6
+ import "./env-b9k1PHMI.js";
7
+ import { n as discoverModels, t as discoverAuthStorage } from "./pi-model-discovery-Dh4ziodY.js";
8
+ export { discoverAuthStorage, discoverModels };
@@ -0,0 +1,349 @@
1
+ import "./paths-BtVqCdw4.js";
2
+ import "./config-B9ODwgpz.js";
3
+ import "./paths-eFexkPEh.js";
4
+ import "./github-copilot-token-Cxf8QYZb.js";
5
+ import { F as isPlainObject, a as createSubsystemLogger } from "./logger-D6zRubj0.js";
6
+ import "./env-b9k1PHMI.js";
7
+ import { p as getDiagnosticSessionState, s as logToolLoopAction } from "./diagnostic-CxIvS-C2.js";
8
+ import { createHash } from "node:crypto";
9
+ //#region src/agents/tool-loop-detection.ts
10
+ const log = createSubsystemLogger("agents/loop-detection");
11
+ const DEFAULT_LOOP_DETECTION_CONFIG = {
12
+ enabled: false,
13
+ historySize: 30,
14
+ warningThreshold: 10,
15
+ criticalThreshold: 20,
16
+ globalCircuitBreakerThreshold: 30,
17
+ detectors: {
18
+ genericRepeat: true,
19
+ knownPollNoProgress: true,
20
+ pingPong: true
21
+ }
22
+ };
23
+ function asPositiveInt(value, fallback) {
24
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return fallback;
25
+ return value;
26
+ }
27
+ function resolveLoopDetectionConfig(config) {
28
+ let warningThreshold = asPositiveInt(config?.warningThreshold, DEFAULT_LOOP_DETECTION_CONFIG.warningThreshold);
29
+ let criticalThreshold = asPositiveInt(config?.criticalThreshold, DEFAULT_LOOP_DETECTION_CONFIG.criticalThreshold);
30
+ let globalCircuitBreakerThreshold = asPositiveInt(config?.globalCircuitBreakerThreshold, DEFAULT_LOOP_DETECTION_CONFIG.globalCircuitBreakerThreshold);
31
+ if (criticalThreshold <= warningThreshold) criticalThreshold = warningThreshold + 1;
32
+ if (globalCircuitBreakerThreshold <= criticalThreshold) globalCircuitBreakerThreshold = criticalThreshold + 1;
33
+ return {
34
+ enabled: config?.enabled ?? DEFAULT_LOOP_DETECTION_CONFIG.enabled,
35
+ historySize: asPositiveInt(config?.historySize, DEFAULT_LOOP_DETECTION_CONFIG.historySize),
36
+ warningThreshold,
37
+ criticalThreshold,
38
+ globalCircuitBreakerThreshold,
39
+ detectors: {
40
+ genericRepeat: config?.detectors?.genericRepeat ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.genericRepeat,
41
+ knownPollNoProgress: config?.detectors?.knownPollNoProgress ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.knownPollNoProgress,
42
+ pingPong: config?.detectors?.pingPong ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.pingPong
43
+ }
44
+ };
45
+ }
46
+ /**
47
+ * Hash a tool call for pattern matching.
48
+ * Uses tool name + deterministic JSON serialization digest of params.
49
+ */
50
+ function hashToolCall(toolName, params) {
51
+ return `${toolName}:${digestStable(params)}`;
52
+ }
53
+ function stableStringify(value) {
54
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
55
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
56
+ const obj = value;
57
+ return `{${Object.keys(obj).toSorted().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
58
+ }
59
+ function digestStable(value) {
60
+ const serialized = stableStringifyFallback(value);
61
+ return createHash("sha256").update(serialized).digest("hex");
62
+ }
63
+ function stableStringifyFallback(value) {
64
+ try {
65
+ return stableStringify(value);
66
+ } catch {
67
+ if (value === null || value === void 0) return `${value}`;
68
+ if (typeof value === "string") return value;
69
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return `${value}`;
70
+ if (value instanceof Error) return `${value.name}:${value.message}`;
71
+ return Object.prototype.toString.call(value);
72
+ }
73
+ }
74
+ function isKnownPollToolCall(toolName, params) {
75
+ if (toolName === "command_status") return true;
76
+ if (toolName !== "process" || !isPlainObject(params)) return false;
77
+ const action = params.action;
78
+ return action === "poll" || action === "log";
79
+ }
80
+ function extractTextContent(result) {
81
+ if (!isPlainObject(result) || !Array.isArray(result.content)) return "";
82
+ return result.content.filter((entry) => isPlainObject(entry) && typeof entry.type === "string" && typeof entry.text === "string").map((entry) => entry.text).join("\n").trim();
83
+ }
84
+ function formatErrorForHash(error) {
85
+ if (error instanceof Error) return error.message || error.name;
86
+ if (typeof error === "string") return error;
87
+ if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return `${error}`;
88
+ return stableStringify(error);
89
+ }
90
+ function hashToolOutcome(toolName, params, result, error) {
91
+ if (error !== void 0) return `error:${digestStable(formatErrorForHash(error))}`;
92
+ if (!isPlainObject(result)) return result === void 0 ? void 0 : digestStable(result);
93
+ const details = isPlainObject(result.details) ? result.details : {};
94
+ const text = extractTextContent(result);
95
+ if (isKnownPollToolCall(toolName, params) && toolName === "process" && isPlainObject(params)) {
96
+ const action = params.action;
97
+ if (action === "poll") return digestStable({
98
+ action,
99
+ status: details.status,
100
+ exitCode: details.exitCode ?? null,
101
+ exitSignal: details.exitSignal ?? null,
102
+ aggregated: details.aggregated ?? null,
103
+ text
104
+ });
105
+ if (action === "log") return digestStable({
106
+ action,
107
+ status: details.status,
108
+ totalLines: details.totalLines ?? null,
109
+ totalChars: details.totalChars ?? null,
110
+ truncated: details.truncated ?? null,
111
+ exitCode: details.exitCode ?? null,
112
+ exitSignal: details.exitSignal ?? null,
113
+ text
114
+ });
115
+ }
116
+ return digestStable({
117
+ details,
118
+ text
119
+ });
120
+ }
121
+ function getNoProgressStreak(history, toolName, argsHash) {
122
+ let streak = 0;
123
+ let latestResultHash;
124
+ for (let i = history.length - 1; i >= 0; i -= 1) {
125
+ const record = history[i];
126
+ if (!record || record.toolName !== toolName || record.argsHash !== argsHash) continue;
127
+ if (typeof record.resultHash !== "string" || !record.resultHash) continue;
128
+ if (!latestResultHash) {
129
+ latestResultHash = record.resultHash;
130
+ streak = 1;
131
+ continue;
132
+ }
133
+ if (record.resultHash !== latestResultHash) break;
134
+ streak += 1;
135
+ }
136
+ return {
137
+ count: streak,
138
+ latestResultHash
139
+ };
140
+ }
141
+ function getPingPongStreak(history, currentSignature) {
142
+ const last = history.at(-1);
143
+ if (!last) return {
144
+ count: 0,
145
+ noProgressEvidence: false
146
+ };
147
+ let otherSignature;
148
+ let otherToolName;
149
+ for (let i = history.length - 2; i >= 0; i -= 1) {
150
+ const call = history[i];
151
+ if (!call) continue;
152
+ if (call.argsHash !== last.argsHash) {
153
+ otherSignature = call.argsHash;
154
+ otherToolName = call.toolName;
155
+ break;
156
+ }
157
+ }
158
+ if (!otherSignature || !otherToolName) return {
159
+ count: 0,
160
+ noProgressEvidence: false
161
+ };
162
+ let alternatingTailCount = 0;
163
+ for (let i = history.length - 1; i >= 0; i -= 1) {
164
+ const call = history[i];
165
+ if (!call) continue;
166
+ const expected = alternatingTailCount % 2 === 0 ? last.argsHash : otherSignature;
167
+ if (call.argsHash !== expected) break;
168
+ alternatingTailCount += 1;
169
+ }
170
+ if (alternatingTailCount < 2) return {
171
+ count: 0,
172
+ noProgressEvidence: false
173
+ };
174
+ if (currentSignature !== otherSignature) return {
175
+ count: 0,
176
+ noProgressEvidence: false
177
+ };
178
+ const tailStart = Math.max(0, history.length - alternatingTailCount);
179
+ let firstHashA;
180
+ let firstHashB;
181
+ let noProgressEvidence = true;
182
+ for (let i = tailStart; i < history.length; i += 1) {
183
+ const call = history[i];
184
+ if (!call) continue;
185
+ if (!call.resultHash) {
186
+ noProgressEvidence = false;
187
+ break;
188
+ }
189
+ if (call.argsHash === last.argsHash) {
190
+ if (!firstHashA) firstHashA = call.resultHash;
191
+ else if (firstHashA !== call.resultHash) {
192
+ noProgressEvidence = false;
193
+ break;
194
+ }
195
+ continue;
196
+ }
197
+ if (call.argsHash === otherSignature) {
198
+ if (!firstHashB) firstHashB = call.resultHash;
199
+ else if (firstHashB !== call.resultHash) {
200
+ noProgressEvidence = false;
201
+ break;
202
+ }
203
+ continue;
204
+ }
205
+ noProgressEvidence = false;
206
+ break;
207
+ }
208
+ if (!firstHashA || !firstHashB) noProgressEvidence = false;
209
+ return {
210
+ count: alternatingTailCount + 1,
211
+ pairedToolName: last.toolName,
212
+ pairedSignature: last.argsHash,
213
+ noProgressEvidence
214
+ };
215
+ }
216
+ function canonicalPairKey(signatureA, signatureB) {
217
+ return [signatureA, signatureB].toSorted().join("|");
218
+ }
219
+ /**
220
+ * Detect if an agent is stuck in a repetitive tool call loop.
221
+ * Checks if the same tool+params combination has been called excessively.
222
+ */
223
+ function detectToolCallLoop(state, toolName, params, config) {
224
+ const resolvedConfig = resolveLoopDetectionConfig(config);
225
+ if (!resolvedConfig.enabled) return { stuck: false };
226
+ const history = state.toolCallHistory ?? [];
227
+ const currentHash = hashToolCall(toolName, params);
228
+ const noProgress = getNoProgressStreak(history, toolName, currentHash);
229
+ const noProgressStreak = noProgress.count;
230
+ const knownPollTool = isKnownPollToolCall(toolName, params);
231
+ const pingPong = getPingPongStreak(history, currentHash);
232
+ if (noProgressStreak >= resolvedConfig.globalCircuitBreakerThreshold) {
233
+ log.error(`Global circuit breaker triggered: ${toolName} repeated ${noProgressStreak} times with no progress`);
234
+ return {
235
+ stuck: true,
236
+ level: "critical",
237
+ detector: "global_circuit_breaker",
238
+ count: noProgressStreak,
239
+ message: `CRITICAL: ${toolName} has repeated identical no-progress outcomes ${noProgressStreak} times. Session execution blocked by global circuit breaker to prevent runaway loops.`,
240
+ warningKey: `global:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
241
+ };
242
+ }
243
+ if (knownPollTool && resolvedConfig.detectors.knownPollNoProgress && noProgressStreak >= resolvedConfig.criticalThreshold) {
244
+ log.error(`Critical polling loop detected: ${toolName} repeated ${noProgressStreak} times`);
245
+ return {
246
+ stuck: true,
247
+ level: "critical",
248
+ detector: "known_poll_no_progress",
249
+ count: noProgressStreak,
250
+ message: `CRITICAL: Called ${toolName} with identical arguments and no progress ${noProgressStreak} times. This appears to be a stuck polling loop. Session execution blocked to prevent resource waste.`,
251
+ warningKey: `poll:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
252
+ };
253
+ }
254
+ if (knownPollTool && resolvedConfig.detectors.knownPollNoProgress && noProgressStreak >= resolvedConfig.warningThreshold) {
255
+ log.warn(`Polling loop warning: ${toolName} repeated ${noProgressStreak} times`);
256
+ return {
257
+ stuck: true,
258
+ level: "warning",
259
+ detector: "known_poll_no_progress",
260
+ count: noProgressStreak,
261
+ message: `WARNING: You have called ${toolName} ${noProgressStreak} times with identical arguments and no progress. Stop polling and either (1) increase wait time between checks, or (2) report the task as failed if the process is stuck.`,
262
+ warningKey: `poll:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
263
+ };
264
+ }
265
+ const pingPongWarningKey = pingPong.pairedSignature ? `pingpong:${canonicalPairKey(currentHash, pingPong.pairedSignature)}` : `pingpong:${toolName}:${currentHash}`;
266
+ if (resolvedConfig.detectors.pingPong && pingPong.count >= resolvedConfig.criticalThreshold && pingPong.noProgressEvidence) {
267
+ log.error(`Critical ping-pong loop detected: alternating calls count=${pingPong.count} currentTool=${toolName}`);
268
+ return {
269
+ stuck: true,
270
+ level: "critical",
271
+ detector: "ping_pong",
272
+ count: pingPong.count,
273
+ message: `CRITICAL: You are alternating between repeated tool-call patterns (${pingPong.count} consecutive calls) with no progress. This appears to be a stuck ping-pong loop. Session execution blocked to prevent resource waste.`,
274
+ pairedToolName: pingPong.pairedToolName,
275
+ warningKey: pingPongWarningKey
276
+ };
277
+ }
278
+ if (resolvedConfig.detectors.pingPong && pingPong.count >= resolvedConfig.warningThreshold) {
279
+ log.warn(`Ping-pong loop warning: alternating calls count=${pingPong.count} currentTool=${toolName}`);
280
+ return {
281
+ stuck: true,
282
+ level: "warning",
283
+ detector: "ping_pong",
284
+ count: pingPong.count,
285
+ message: `WARNING: You are alternating between repeated tool-call patterns (${pingPong.count} consecutive calls). This looks like a ping-pong loop; stop retrying and report the task as failed.`,
286
+ pairedToolName: pingPong.pairedToolName,
287
+ warningKey: pingPongWarningKey
288
+ };
289
+ }
290
+ const recentCount = history.filter((h) => h.toolName === toolName && h.argsHash === currentHash).length;
291
+ if (!knownPollTool && resolvedConfig.detectors.genericRepeat && recentCount >= resolvedConfig.warningThreshold) {
292
+ log.warn(`Loop warning: ${toolName} called ${recentCount} times with identical arguments`);
293
+ return {
294
+ stuck: true,
295
+ level: "warning",
296
+ detector: "generic_repeat",
297
+ count: recentCount,
298
+ message: `WARNING: You have called ${toolName} ${recentCount} times with identical arguments. If this is not making progress, stop retrying and report the task as failed.`,
299
+ warningKey: `generic:${toolName}:${currentHash}`
300
+ };
301
+ }
302
+ return { stuck: false };
303
+ }
304
+ /**
305
+ * Record a tool call in the session's history for loop detection.
306
+ * Maintains sliding window of last N calls.
307
+ */
308
+ function recordToolCall(state, toolName, params, toolCallId, config) {
309
+ const resolvedConfig = resolveLoopDetectionConfig(config);
310
+ if (!state.toolCallHistory) state.toolCallHistory = [];
311
+ state.toolCallHistory.push({
312
+ toolName,
313
+ argsHash: hashToolCall(toolName, params),
314
+ toolCallId,
315
+ timestamp: Date.now()
316
+ });
317
+ if (state.toolCallHistory.length > resolvedConfig.historySize) state.toolCallHistory.shift();
318
+ }
319
+ /**
320
+ * Record a completed tool call outcome so loop detection can identify no-progress repeats.
321
+ */
322
+ function recordToolCallOutcome(state, params) {
323
+ const resolvedConfig = resolveLoopDetectionConfig(params.config);
324
+ const resultHash = hashToolOutcome(params.toolName, params.toolParams, params.result, params.error);
325
+ if (!resultHash) return;
326
+ if (!state.toolCallHistory) state.toolCallHistory = [];
327
+ const argsHash = hashToolCall(params.toolName, params.toolParams);
328
+ let matched = false;
329
+ for (let i = state.toolCallHistory.length - 1; i >= 0; i -= 1) {
330
+ const call = state.toolCallHistory[i];
331
+ if (!call) continue;
332
+ if (params.toolCallId && call.toolCallId !== params.toolCallId) continue;
333
+ if (call.toolName !== params.toolName || call.argsHash !== argsHash) continue;
334
+ if (call.resultHash !== void 0) continue;
335
+ call.resultHash = resultHash;
336
+ matched = true;
337
+ break;
338
+ }
339
+ if (!matched) state.toolCallHistory.push({
340
+ toolName: params.toolName,
341
+ argsHash,
342
+ toolCallId: params.toolCallId,
343
+ resultHash,
344
+ timestamp: Date.now()
345
+ });
346
+ if (state.toolCallHistory.length > resolvedConfig.historySize) state.toolCallHistory.splice(0, state.toolCallHistory.length - resolvedConfig.historySize);
347
+ }
348
+ //#endregion
349
+ export { detectToolCallLoop, getDiagnosticSessionState, logToolLoopAction, recordToolCall, recordToolCallOutcome };
@@ -0,0 +1,54 @@
1
+ import { i as logWarn } from "./logger-D6zRubj0.js";
2
+ import { EnvHttpProxyAgent, ProxyAgent, fetch } from "undici";
3
+ //#region src/infra/net/proxy-fetch.ts
4
+ const PROXY_FETCH_PROXY_URL = Symbol.for("openclaw.proxyFetch.proxyUrl");
5
+ /**
6
+ * Create a fetch function that routes requests through the given HTTP proxy.
7
+ * Uses undici's ProxyAgent under the hood.
8
+ */
9
+ function makeProxyFetch(proxyUrl) {
10
+ let agent = null;
11
+ const resolveAgent = () => {
12
+ if (!agent) agent = new ProxyAgent(proxyUrl);
13
+ return agent;
14
+ };
15
+ const proxyFetch = ((input, init) => fetch(input, {
16
+ ...init,
17
+ dispatcher: resolveAgent()
18
+ }));
19
+ Object.defineProperty(proxyFetch, PROXY_FETCH_PROXY_URL, {
20
+ value: proxyUrl,
21
+ enumerable: false,
22
+ configurable: false,
23
+ writable: false
24
+ });
25
+ return proxyFetch;
26
+ }
27
+ function getProxyUrlFromFetch(fetchImpl) {
28
+ const proxyUrl = fetchImpl?.[PROXY_FETCH_PROXY_URL];
29
+ if (typeof proxyUrl !== "string") return;
30
+ const trimmed = proxyUrl.trim();
31
+ return trimmed ? trimmed : void 0;
32
+ }
33
+ /**
34
+ * Resolve a proxy-aware fetch from standard environment variables
35
+ * (HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy).
36
+ * Respects NO_PROXY / no_proxy exclusions via undici's EnvHttpProxyAgent.
37
+ * Returns undefined when no proxy is configured.
38
+ * Gracefully returns undefined if the proxy URL is malformed.
39
+ */
40
+ function resolveProxyFetchFromEnv() {
41
+ if (!(process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.https_proxy || process.env.http_proxy)?.trim()) return;
42
+ try {
43
+ const agent = new EnvHttpProxyAgent();
44
+ return ((input, init) => fetch(input, {
45
+ ...init,
46
+ dispatcher: agent
47
+ }));
48
+ } catch (err) {
49
+ logWarn(`Proxy env var set but agent creation failed — falling back to direct fetch: ${err instanceof Error ? err.message : String(err)}`);
50
+ return;
51
+ }
52
+ }
53
+ //#endregion
54
+ export { makeProxyFetch as n, resolveProxyFetchFromEnv as r, getProxyUrlFromFetch as t };