@farazirfan/costar-server-executor 1.7.29 → 1.7.31
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/agent/agent.d.ts +105 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +534 -0
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/model-fallback.d.ts.map +1 -1
- package/dist/agent/model-fallback.js +14 -0
- package/dist/agent/model-fallback.js.map +1 -1
- package/dist/agent/pi-embedded-runner/compact.d.ts +7 -0
- package/dist/agent/pi-embedded-runner/compact.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/compact.js +100 -4
- package/dist/agent/pi-embedded-runner/compact.js.map +1 -1
- package/dist/agent/pi-embedded-runner/run.d.ts +7 -0
- package/dist/agent/pi-embedded-runner/run.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/run.js +373 -112
- package/dist/agent/pi-embedded-runner/run.js.map +1 -1
- package/dist/agent/pi-embedded-runner/subscribe.d.ts +14 -0
- package/dist/agent/pi-embedded-runner/subscribe.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/subscribe.js +54 -2
- package/dist/agent/pi-embedded-runner/subscribe.js.map +1 -1
- package/dist/agent/pi-embedded-runner/tool-result-context-guard.d.ts +33 -0
- package/dist/agent/pi-embedded-runner/tool-result-context-guard.d.ts.map +1 -0
- package/dist/agent/pi-embedded-runner/tool-result-context-guard.js +287 -0
- package/dist/agent/pi-embedded-runner/tool-result-context-guard.js.map +1 -0
- package/dist/agent/pi-embedded-runner/tools.d.ts +8 -1
- package/dist/agent/pi-embedded-runner/tools.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/tools.js +10 -2
- package/dist/agent/pi-embedded-runner/tools.js.map +1 -1
- package/dist/agent/pi-embedded-runner/types.d.ts +11 -0
- package/dist/agent/pi-embedded-runner/types.d.ts.map +1 -1
- package/dist/api/chat.d.ts.map +1 -1
- package/dist/api/chat.js +13 -0
- package/dist/api/chat.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cron/index.d.ts +1 -1
- package/dist/cron/index.d.ts.map +1 -1
- package/dist/cron/schedule.d.ts +46 -0
- package/dist/cron/schedule.d.ts.map +1 -0
- package/dist/cron/schedule.js +109 -0
- package/dist/cron/schedule.js.map +1 -0
- package/dist/cron/scheduler.d.ts +86 -40
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +525 -159
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/cron/types.d.ts +16 -0
- package/dist/cron/types.d.ts.map +1 -1
- package/dist/heartbeat/runner.d.ts +9 -4
- package/dist/heartbeat/runner.d.ts.map +1 -1
- package/dist/heartbeat/runner.js +116 -48
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -3
- package/dist/server.js.map +1 -1
- package/dist/supabase/cron-jobs.d.ts.map +1 -1
- package/dist/supabase/cron-jobs.js +16 -6
- package/dist/supabase/cron-jobs.js.map +1 -1
- package/package.json +5 -4
- package/public/index.html +177 -56
- package/skills/okx/LEARNING.md +41 -0
- package/skills/okx/SKILL.md +231 -0
- package/skills/okx/references/api-reference.md +201 -0
- package/skills/okx/references/order-types.md +251 -0
- package/skills/okx/references/products.md +128 -0
- package/skills/okx/scripts/okx-trade.ts +273 -0
- package/skills/trading/SKILL.md +6 -4
|
@@ -20,9 +20,11 @@ import { prepareSessionManagerForRun, getSessionDiagnostics } from "./session-ma
|
|
|
20
20
|
import { repairSessionFileIfNeeded } from "./session-file-repair.js";
|
|
21
21
|
import { guardSessionManager } from "./session-tool-result-guard.js";
|
|
22
22
|
import { acquireSessionWriteLock } from "./session-write-lock.js";
|
|
23
|
-
import { isContextOverflowError, isCompactionFailureError, describeUnknownError, } from "../pi-embedded-helpers/errors.js";
|
|
23
|
+
import { isContextOverflowError, isLikelyContextOverflowError, isCompactionFailureError, describeUnknownError, } from "../pi-embedded-helpers/errors.js";
|
|
24
|
+
import { FailoverError, classifyFailoverReason } from "../model-fallback.js";
|
|
24
25
|
import { compactEmbeddedPiSessionDirect } from "./compact.js";
|
|
25
26
|
import { truncateOversizedToolResultsInSession, sessionLikelyHasOversizedToolResults, } from "./tool-result-truncation.js";
|
|
27
|
+
import { installToolResultContextGuard } from "./tool-result-context-guard.js";
|
|
26
28
|
// Re-export for backwards compatibility (used by existing tests)
|
|
27
29
|
export { isContextOverflowError } from "../pi-embedded-helpers/errors.js";
|
|
28
30
|
/**
|
|
@@ -69,23 +71,34 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
69
71
|
let overflowCompactionAttempts = 0;
|
|
70
72
|
let toolResultTruncationAttempted = false;
|
|
71
73
|
// Retry loop for context overflow + auto-compaction (following OpenClaw's pattern)
|
|
74
|
+
//
|
|
75
|
+
// Recovery strategy (ported from OpenClaw run.ts lines 480-761):
|
|
76
|
+
//
|
|
77
|
+
// 1. Run attempt → detect overflow from promptError, assistantError, or zero-token
|
|
78
|
+
// 2. If the SDK already auto-compacted during the attempt (compactionCount > 0),
|
|
79
|
+
// just retry the prompt without additional explicit compaction.
|
|
80
|
+
// 3. Otherwise, try explicit compaction via compactEmbeddedPiSessionDirect().
|
|
81
|
+
// 4. **CRITICAL FIX**: If compaction FAILS (e.g., summarization prompt too large at 228K > 200K),
|
|
82
|
+
// immediately try truncating oversized tool results in the session file. This shrinks
|
|
83
|
+
// the session so the NEXT compaction attempt can succeed.
|
|
84
|
+
// 5. After successful truncation, reset the compaction counter so we can retry compaction.
|
|
85
|
+
// 6. Last resort: reset session file.
|
|
72
86
|
while (true) {
|
|
73
87
|
await fs.mkdir(params.workspaceDir, { recursive: true });
|
|
74
88
|
const result = await runEmbeddedAttempt(params, model, authStorage, modelRegistry);
|
|
75
89
|
const { aborted, promptError, sessionIdUsed, assistantTexts } = result;
|
|
90
|
+
const attemptCompactionCount = Math.max(0, result.compactionCount ?? 0);
|
|
76
91
|
// Extract assistant error text (following OpenClaw's pattern)
|
|
77
92
|
const assistantErrorText = result.error ?? undefined;
|
|
78
93
|
// --- Context overflow detection (following OpenClaw lines 473-489) ---
|
|
79
94
|
// Check promptError first; only check assistantError if no promptError
|
|
80
|
-
|
|
95
|
+
let contextOverflowError = !aborted
|
|
81
96
|
? (() => {
|
|
82
97
|
if (promptError) {
|
|
83
98
|
const errorText = describeUnknownError(promptError);
|
|
84
99
|
if (isContextOverflowError(errorText)) {
|
|
85
100
|
return { text: errorText, source: "promptError" };
|
|
86
101
|
}
|
|
87
|
-
// Prompt submission failed with a non-overflow error. Do not
|
|
88
|
-
// inspect prior assistant errors from history for this attempt.
|
|
89
102
|
return null;
|
|
90
103
|
}
|
|
91
104
|
if (assistantErrorText && isContextOverflowError(assistantErrorText)) {
|
|
@@ -104,116 +117,48 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
104
117
|
inputTokens: result.inputTokens,
|
|
105
118
|
outputTokens: result.outputTokens,
|
|
106
119
|
});
|
|
107
|
-
if (isZeroToken) {
|
|
120
|
+
if (isZeroToken && !contextOverflowError) {
|
|
108
121
|
console.warn(`[PI_AGENT] [zero-token-death-spiral] sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
109
122
|
`provider=${provider}/${modelId} sessionFile=${params.sessionFile} ` +
|
|
110
123
|
`compactionAttempts=${overflowCompactionAttempts} — treating as context overflow`);
|
|
111
|
-
|
|
112
|
-
const syntheticError = {
|
|
124
|
+
contextOverflowError = {
|
|
113
125
|
text: "Zero-token response detected (likely context overflow)",
|
|
114
126
|
source: "zeroToken",
|
|
115
127
|
};
|
|
116
|
-
// Fall through to the compaction logic below
|
|
117
|
-
if (contextOverflowError) {
|
|
118
|
-
// Already have an overflow error; just log the zero-token detection
|
|
119
|
-
console.warn(`[PI_AGENT] Zero-token detected in addition to ${contextOverflowError.source}`);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
// No explicit overflow error; treat zero-token as overflow
|
|
123
|
-
const errorText = syntheticError.text;
|
|
124
|
-
console.warn(`[PI_AGENT] [context-overflow-diag] sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
125
|
-
`provider=${provider}/${modelId} source=${syntheticError.source} ` +
|
|
126
|
-
`sessionFile=${params.sessionFile} ` +
|
|
127
|
-
`compactionAttempts=${overflowCompactionAttempts} error=${errorText}`);
|
|
128
|
-
const isCompactionFailure = false; // Zero-token is never a compaction failure
|
|
129
|
-
// Attempt auto-compaction on zero-token (same logic as context overflow)
|
|
130
|
-
if (!isCompactionFailure &&
|
|
131
|
-
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS) {
|
|
132
|
-
overflowCompactionAttempts++;
|
|
133
|
-
console.warn(`[PI_AGENT] zero-token overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); attempting auto-compaction for ${provider}/${modelId}`);
|
|
134
|
-
const compactResult = await compactEmbeddedPiSessionDirect({
|
|
135
|
-
sessionId: params.sessionId,
|
|
136
|
-
sessionKey: params.sessionKey,
|
|
137
|
-
sessionFile: params.sessionFile,
|
|
138
|
-
workspaceDir: params.workspaceDir,
|
|
139
|
-
agentDir: params.agentDir,
|
|
140
|
-
config: params.config,
|
|
141
|
-
provider,
|
|
142
|
-
model: modelId,
|
|
143
|
-
thinkLevel: params.thinkLevel,
|
|
144
|
-
extraSystemPrompt: params.extraSystemPrompt,
|
|
145
|
-
skillEntries: params.skillEntries,
|
|
146
|
-
authProfileId: params.authProfileId,
|
|
147
|
-
cronDeps: params.cronDeps,
|
|
148
|
-
});
|
|
149
|
-
if (compactResult.compacted) {
|
|
150
|
-
console.log(`[PI_AGENT] auto-compaction succeeded for ${provider}/${modelId}; retrying prompt`);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
console.warn(`[PI_AGENT] auto-compaction failed for ${provider}/${modelId}: ${compactResult.reason ?? "nothing to compact"}`);
|
|
154
|
-
}
|
|
155
|
-
// Fallback: try truncating oversized tool results in the session.
|
|
156
|
-
if (!toolResultTruncationAttempted) {
|
|
157
|
-
const hasOversized = result.messagesSnapshot
|
|
158
|
-
? sessionLikelyHasOversizedToolResults({
|
|
159
|
-
messages: result.messagesSnapshot,
|
|
160
|
-
contextWindowTokens,
|
|
161
|
-
})
|
|
162
|
-
: false;
|
|
163
|
-
if (hasOversized) {
|
|
164
|
-
toolResultTruncationAttempted = true;
|
|
165
|
-
console.warn(`[PI_AGENT] [context-overflow-recovery] Attempting tool result truncation for ${provider}/${modelId} ` +
|
|
166
|
-
`(contextWindow=${contextWindowTokens} tokens)`);
|
|
167
|
-
const truncResult = await truncateOversizedToolResultsInSession({
|
|
168
|
-
sessionFile: params.sessionFile,
|
|
169
|
-
contextWindowTokens,
|
|
170
|
-
sessionId: params.sessionId,
|
|
171
|
-
sessionKey: params.sessionKey,
|
|
172
|
-
});
|
|
173
|
-
if (truncResult.truncated) {
|
|
174
|
-
console.log(`[PI_AGENT] [context-overflow-recovery] Truncated ${truncResult.truncatedCount} tool result(s); retrying prompt`);
|
|
175
|
-
// Session is now smaller; allow compaction retries again.
|
|
176
|
-
overflowCompactionAttempts = 0;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
console.warn(`[PI_AGENT] [context-overflow-recovery] Tool result truncation did not help: ${truncResult.reason ?? "unknown"}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// CoStar-specific: reset session as absolute last resort
|
|
183
|
-
console.warn(`[PI_AGENT] Zero-token overflow unrecoverable; resetting session`);
|
|
184
|
-
await resetSessionFile(params.sessionFile);
|
|
185
|
-
return {
|
|
186
|
-
payloads: [
|
|
187
|
-
{
|
|
188
|
-
text: "Context overflow: prompt too large for the model (zero-token response). " +
|
|
189
|
-
"I've reset my session to continue. What would you like me to do?",
|
|
190
|
-
isError: true,
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
meta: {
|
|
194
|
-
durationMs: Date.now() - started,
|
|
195
|
-
agentMeta: {
|
|
196
|
-
sessionId: sessionIdUsed,
|
|
197
|
-
provider,
|
|
198
|
-
model: model.id,
|
|
199
|
-
},
|
|
200
|
-
error: { kind: "context_overflow", message: errorText },
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
128
|
}
|
|
129
|
+
else if (isZeroToken && contextOverflowError) {
|
|
130
|
+
console.warn(`[PI_AGENT] Zero-token detected in addition to ${contextOverflowError.source}`);
|
|
131
|
+
}
|
|
132
|
+
// --- Unified overflow recovery (ported from OpenClaw run.ts lines 599-761) ---
|
|
205
133
|
if (contextOverflowError) {
|
|
206
134
|
const errorText = contextOverflowError.text;
|
|
207
135
|
console.warn(`[PI_AGENT] [context-overflow-diag] sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
208
136
|
`provider=${provider}/${modelId} source=${contextOverflowError.source} ` +
|
|
209
137
|
`sessionFile=${params.sessionFile} ` +
|
|
210
|
-
`compactionAttempts=${overflowCompactionAttempts}
|
|
138
|
+
`compactionAttempts=${overflowCompactionAttempts} ` +
|
|
139
|
+
`attemptCompactionCount=${attemptCompactionCount} error=${errorText.slice(0, 200)}`);
|
|
211
140
|
const isCompactionFailure = isCompactionFailureError(errorText);
|
|
212
|
-
|
|
141
|
+
const hadAttemptLevelCompaction = attemptCompactionCount > 0;
|
|
142
|
+
// BRANCH 1 (OpenClaw pattern): If the SDK already auto-compacted during
|
|
143
|
+
// this attempt, avoid immediately running another explicit compaction.
|
|
144
|
+
// Just retry the prompt — the compaction may have helped enough.
|
|
145
|
+
if (!isCompactionFailure &&
|
|
146
|
+
hadAttemptLevelCompaction &&
|
|
147
|
+
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS) {
|
|
148
|
+
overflowCompactionAttempts++;
|
|
149
|
+
console.warn(`[PI_AGENT] context overflow persisted after in-attempt compaction ` +
|
|
150
|
+
`(attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); ` +
|
|
151
|
+
`retrying prompt without additional compaction for ${provider}/${modelId}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// BRANCH 2: Explicit overflow compaction — only when the attempt did NOT
|
|
155
|
+
// already auto-compact (avoids redundant back-to-back compactions).
|
|
213
156
|
if (!isCompactionFailure &&
|
|
157
|
+
!hadAttemptLevelCompaction &&
|
|
214
158
|
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS) {
|
|
215
159
|
overflowCompactionAttempts++;
|
|
216
|
-
console.warn(`[PI_AGENT] context overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS});
|
|
160
|
+
console.warn(`[PI_AGENT] context overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); ` +
|
|
161
|
+
`attempting explicit compaction for ${provider}/${modelId}`);
|
|
217
162
|
const compactResult = await compactEmbeddedPiSessionDirect({
|
|
218
163
|
sessionId: params.sessionId,
|
|
219
164
|
sessionKey: params.sessionKey,
|
|
@@ -234,10 +179,25 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
234
179
|
continue;
|
|
235
180
|
}
|
|
236
181
|
console.warn(`[PI_AGENT] auto-compaction failed for ${provider}/${modelId}: ${compactResult.reason ?? "nothing to compact"}`);
|
|
182
|
+
// If the compaction failure reason is itself a compaction failure error
|
|
183
|
+
// (e.g., summarization prompt too long at 228K > 200K limit),
|
|
184
|
+
// exhaust the retry counter to avoid pointless retries.
|
|
185
|
+
if (compactResult.reason && isCompactionFailureError(compactResult.reason)) {
|
|
186
|
+
console.warn(`[PI_AGENT] Compaction failure is a summarization-too-large error — ` +
|
|
187
|
+
`exhausting compaction retries, will try tool result truncation next`);
|
|
188
|
+
overflowCompactionAttempts = MAX_OVERFLOW_COMPACTION_ATTEMPTS;
|
|
189
|
+
}
|
|
237
190
|
}
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// context window
|
|
191
|
+
// BRANCH 3: Tool result truncation fallback.
|
|
192
|
+
// **CRITICAL FIX**: When compaction fails because the summarization prompt
|
|
193
|
+
// itself exceeds the context window (e.g., 228K > 200K), we MUST truncate
|
|
194
|
+
// oversized tool results FIRST to shrink the session. After truncation
|
|
195
|
+
// succeeds, we reset the compaction counter so the NEXT iteration can
|
|
196
|
+
// successfully compact the now-smaller session.
|
|
197
|
+
//
|
|
198
|
+
// This prevents the "compaction can't compact because session is too big
|
|
199
|
+
// to even summarize" deadlock that causes the zero-token death spiral
|
|
200
|
+
// and session resets.
|
|
241
201
|
if (!toolResultTruncationAttempted) {
|
|
242
202
|
const hasOversized = result.messagesSnapshot
|
|
243
203
|
? sessionLikelyHasOversizedToolResults({
|
|
@@ -256,8 +216,11 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
256
216
|
sessionKey: params.sessionKey,
|
|
257
217
|
});
|
|
258
218
|
if (truncResult.truncated) {
|
|
259
|
-
console.log(`[PI_AGENT] [context-overflow-recovery] Truncated ${truncResult.truncatedCount} tool result(s);
|
|
260
|
-
|
|
219
|
+
console.log(`[PI_AGENT] [context-overflow-recovery] Truncated ${truncResult.truncatedCount} tool result(s); ` +
|
|
220
|
+
`resetting compaction counter and retrying prompt`);
|
|
221
|
+
// Session is now smaller — allow compaction retries again.
|
|
222
|
+
// This is the key: after truncation, the next compaction
|
|
223
|
+
// attempt will have a much smaller session to summarize.
|
|
261
224
|
overflowCompactionAttempts = 0;
|
|
262
225
|
continue;
|
|
263
226
|
}
|
|
@@ -289,9 +252,25 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
289
252
|
};
|
|
290
253
|
}
|
|
291
254
|
// --- Non-overflow error from promptError (following OpenClaw pattern) ---
|
|
255
|
+
// Classify the error: if it's a rate limit, auth, billing, timeout, or overload
|
|
256
|
+
// error, wrap it in FailoverError so model-fallback.ts can try the next model.
|
|
257
|
+
// Context overflow errors are NOT wrapped — they were already handled above.
|
|
292
258
|
if (promptError) {
|
|
293
|
-
const
|
|
294
|
-
console.error(`[PI_AGENT] Prompt error:`,
|
|
259
|
+
const errorMsg = describeUnknownError(promptError);
|
|
260
|
+
console.error(`[PI_AGENT] Prompt error:`, errorMsg);
|
|
261
|
+
// Never wrap context overflow errors as failover — they need compaction, not model switch
|
|
262
|
+
if (!isLikelyContextOverflowError(errorMsg)) {
|
|
263
|
+
const failoverReason = classifyFailoverReason(errorMsg);
|
|
264
|
+
if (failoverReason) {
|
|
265
|
+
console.warn(`[PI_AGENT] Wrapping prompt error as FailoverError (reason: ${failoverReason}) for model fallback`);
|
|
266
|
+
throw new FailoverError(errorMsg, {
|
|
267
|
+
reason: failoverReason,
|
|
268
|
+
provider,
|
|
269
|
+
model: modelId,
|
|
270
|
+
cause: promptError,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
295
274
|
throw promptError;
|
|
296
275
|
}
|
|
297
276
|
// --- Success path ---
|
|
@@ -445,8 +424,10 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
445
424
|
console.log(`[PI_ATTEMPT] Using auth storage for API keys`);
|
|
446
425
|
// Convert CoStar tools to ToolDefinition format (following OpenClaw's pattern:
|
|
447
426
|
// tools are created inside the runner with full context, including cronDeps)
|
|
448
|
-
const customTools = getCustomTools(params.cronDeps
|
|
449
|
-
|
|
427
|
+
const customTools = getCustomTools(params.cronDeps, {
|
|
428
|
+
disableMessageTool: params.disableMessageTool,
|
|
429
|
+
});
|
|
430
|
+
console.log(`[PI_ATTEMPT] Loaded ${customTools.length} custom tools${params.disableMessageTool ? " (message tool disabled)" : ""}`);
|
|
450
431
|
// Configure compaction reserve tokens (following OpenClaw's pattern)
|
|
451
432
|
const DEFAULT_COMPACTION_RESERVE_TOKENS = 20_000;
|
|
452
433
|
const currentReserveTokens = settingsManager.getCompactionReserveTokens();
|
|
@@ -486,6 +467,18 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
486
467
|
if (!session) {
|
|
487
468
|
throw new Error("Failed to create agent session");
|
|
488
469
|
}
|
|
470
|
+
// Install tool result context guard (ported from OpenClaw run/attempt.ts lines 595-603).
|
|
471
|
+
// Prevents individual tool results from growing unbounded during execution,
|
|
472
|
+
// which would eventually cause the session to exceed the model's context window
|
|
473
|
+
// and make the summarization prompt itself too large to send.
|
|
474
|
+
const contextWindowTokens = model.contextWindow ?? DEFAULT_CONTEXT_TOKENS;
|
|
475
|
+
let removeToolResultContextGuard;
|
|
476
|
+
if (session.agent) {
|
|
477
|
+
removeToolResultContextGuard = installToolResultContextGuard({
|
|
478
|
+
agent: session.agent,
|
|
479
|
+
contextWindowTokens,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
489
482
|
let aborted = false;
|
|
490
483
|
// Subscribe to agent events (following OpenClaw's pattern)
|
|
491
484
|
const subscription = subscribeEmbeddedPiSession({
|
|
@@ -495,19 +488,83 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
495
488
|
onToolResult: params.onToolResult,
|
|
496
489
|
onToolStart: params.onToolStart,
|
|
497
490
|
onToolEnd: params.onToolEnd,
|
|
491
|
+
onCompactionStart: params.onCompactionStart ? () => params.onCompactionStart() : undefined,
|
|
492
|
+
onCompactionEnd: params.onCompactionEnd,
|
|
498
493
|
});
|
|
499
494
|
const { assistantTexts, unsubscribe, waitForCompactionRetry } = subscription;
|
|
500
495
|
try {
|
|
496
|
+
// Sanitize session history before prompt submission (OpenClaw pattern)
|
|
497
|
+
// Repairs orphaned tool results, missing tool results, duplicate tool results,
|
|
498
|
+
// and role alternation violations that would cause API 400 errors.
|
|
499
|
+
try {
|
|
500
|
+
const currentMessages = session.messages ?? session.agent?.messages;
|
|
501
|
+
if (Array.isArray(currentMessages) && currentMessages.length > 0) {
|
|
502
|
+
const sanitized = sanitizeSessionMessages(currentMessages);
|
|
503
|
+
if (sanitized !== currentMessages && sanitized.length !== currentMessages.length) {
|
|
504
|
+
const replaceMessages = session.agent?.replaceMessages;
|
|
505
|
+
if (typeof replaceMessages === "function") {
|
|
506
|
+
replaceMessages.call(session.agent, sanitized);
|
|
507
|
+
console.log(`[PI_ATTEMPT] Session sanitized: ${currentMessages.length} → ${sanitized.length} messages`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (sanitizeErr) {
|
|
513
|
+
// Best-effort — don't block the prompt on sanitization failure
|
|
514
|
+
console.warn(`[PI_ATTEMPT] Session sanitization failed (non-critical):`, sanitizeErr);
|
|
515
|
+
}
|
|
501
516
|
// Convert images to pi-ai format
|
|
502
517
|
const piImages = params.images?.map((img) => ({
|
|
503
518
|
type: "image",
|
|
504
519
|
data: img.data,
|
|
505
520
|
mimeType: img.mediaType,
|
|
506
521
|
}));
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
522
|
+
// --- Abort / timeout mechanism (ported from OpenClaw attempt.ts) ---
|
|
523
|
+
// Wrap session.prompt() with an AbortController so we can enforce
|
|
524
|
+
// params.timeoutMs and honour the caller's abortSignal.
|
|
525
|
+
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes default
|
|
526
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
527
|
+
let promptTimedOut = false;
|
|
528
|
+
// Forward caller's abort signal to session.abort() (pi-ai level abort)
|
|
529
|
+
let callerAbortCleanup;
|
|
530
|
+
if (params.abortSignal) {
|
|
531
|
+
if (params.abortSignal.aborted) {
|
|
532
|
+
aborted = true;
|
|
533
|
+
session.abort?.();
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
const onCallerAbort = () => {
|
|
537
|
+
aborted = true;
|
|
538
|
+
session.abort?.();
|
|
539
|
+
};
|
|
540
|
+
params.abortSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
541
|
+
callerAbortCleanup = () => params.abortSignal.removeEventListener("abort", onCallerAbort);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Timeout timer — calls session.abort() on expiry
|
|
545
|
+
const timeoutTimer = setTimeout(() => {
|
|
546
|
+
promptTimedOut = true;
|
|
547
|
+
aborted = true;
|
|
548
|
+
console.warn(`[PI_ATTEMPT] Prompt timed out after ${timeoutMs}ms — aborting session`);
|
|
549
|
+
session.abort?.();
|
|
550
|
+
}, timeoutMs);
|
|
551
|
+
// Don't keep the process alive just for the timeout timer
|
|
552
|
+
if (typeof timeoutTimer === "object" && "unref" in timeoutTimer) {
|
|
553
|
+
timeoutTimer.unref();
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
// Use the agent session to run the prompt
|
|
557
|
+
await session.prompt(params.prompt, {
|
|
558
|
+
images: piImages,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
clearTimeout(timeoutTimer);
|
|
563
|
+
callerAbortCleanup?.();
|
|
564
|
+
}
|
|
565
|
+
if (promptTimedOut) {
|
|
566
|
+
aborted = true;
|
|
567
|
+
}
|
|
511
568
|
// Wait for any pending compaction retries (following OpenClaw's pattern)
|
|
512
569
|
try {
|
|
513
570
|
await waitForCompactionRetry();
|
|
@@ -518,6 +575,13 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
518
575
|
// Get token usage from subscription
|
|
519
576
|
const inputTokens = subscription.getInputTokens();
|
|
520
577
|
const outputTokens = subscription.getOutputTokens();
|
|
578
|
+
// Log any library-level compaction error for observability.
|
|
579
|
+
// The actual error will surface via session.prompt() throwing (promptError),
|
|
580
|
+
// which is handled in the catch block below.
|
|
581
|
+
const libraryCompactionError = subscription.getCompactionError();
|
|
582
|
+
if (libraryCompactionError) {
|
|
583
|
+
console.warn(`[PI_ATTEMPT] Library auto-compaction failed during prompt (will surface via promptError if fatal): ${libraryCompactionError}`);
|
|
584
|
+
}
|
|
521
585
|
return {
|
|
522
586
|
aborted,
|
|
523
587
|
promptError: undefined,
|
|
@@ -526,6 +590,7 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
526
590
|
inputTokens,
|
|
527
591
|
outputTokens,
|
|
528
592
|
messagesSnapshot: session.messages,
|
|
593
|
+
compactionCount: subscription.getCompactionCount(),
|
|
529
594
|
};
|
|
530
595
|
}
|
|
531
596
|
catch (error) {
|
|
@@ -544,14 +609,21 @@ async function runEmbeddedAttempt(params, model, authStorage, modelRegistry) {
|
|
|
544
609
|
outputTokens: subscription.getOutputTokens(),
|
|
545
610
|
error: message,
|
|
546
611
|
messagesSnapshot: [],
|
|
612
|
+
compactionCount: subscription.getCompactionCount(),
|
|
547
613
|
};
|
|
548
614
|
}
|
|
549
615
|
finally {
|
|
616
|
+
// Remove tool result context guard (restore original transformContext)
|
|
617
|
+
removeToolResultContextGuard?.();
|
|
550
618
|
// Unsubscribe from events (like OpenClaw does)
|
|
551
619
|
unsubscribe();
|
|
552
620
|
// Always dispose the session (like OpenClaw does)
|
|
553
621
|
try {
|
|
554
|
-
|
|
622
|
+
// BUGFIX (OpenClaw #8643): Wait for the agent to be truly idle before flushing
|
|
623
|
+
// pending tool results. Without this wait, flushPendingToolResults() fires while
|
|
624
|
+
// tools are still executing during auto-retry, inserting synthetic "missing tool
|
|
625
|
+
// result" errors and causing silent agent failures.
|
|
626
|
+
await flushPendingToolResultsAfterIdle(session, sessionManager);
|
|
555
627
|
session.dispose();
|
|
556
628
|
// Log post-dispose diagnostics to verify persistence
|
|
557
629
|
const diagPost = getSessionDiagnostics(sessionManager);
|
|
@@ -607,4 +679,193 @@ function mapThinkingLevel(level) {
|
|
|
607
679
|
return "off";
|
|
608
680
|
}
|
|
609
681
|
}
|
|
682
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
683
|
+
// Idle-aware tool result flushing (ported from OpenClaw #8643)
|
|
684
|
+
// Pattern: openclaw/src/agents/pi-embedded-runner/wait-for-idle-before-flush.ts
|
|
685
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
686
|
+
/** Max time to wait for agent idle before force-flushing (30s safety timeout) */
|
|
687
|
+
const WAIT_FOR_IDLE_TIMEOUT_MS = 30_000;
|
|
688
|
+
/**
|
|
689
|
+
* Wait for the agent to be truly idle, then flush pending tool results.
|
|
690
|
+
*
|
|
691
|
+
* BUGFIX (OpenClaw #8643): pi-agent-core's auto-retry resolves waitForRetry()
|
|
692
|
+
* on assistant message receipt, *before* tool execution completes in the retried
|
|
693
|
+
* agent loop. Without this wait, flushPendingToolResults() fires while tools are
|
|
694
|
+
* still executing, inserting synthetic "missing tool result" errors and causing
|
|
695
|
+
* silent agent failures.
|
|
696
|
+
*
|
|
697
|
+
* Pattern: OpenClaw's flushPendingToolResultsAfterIdle
|
|
698
|
+
*/
|
|
699
|
+
async function flushPendingToolResultsAfterIdle(session, sessionManager) {
|
|
700
|
+
// Wait for agent idle (best-effort, with safety timeout)
|
|
701
|
+
const waitForIdle = session?.agent?.waitForIdle;
|
|
702
|
+
if (typeof waitForIdle === "function") {
|
|
703
|
+
try {
|
|
704
|
+
await Promise.race([
|
|
705
|
+
waitForIdle.call(session.agent),
|
|
706
|
+
new Promise((resolve) => {
|
|
707
|
+
const timer = setTimeout(resolve, WAIT_FOR_IDLE_TIMEOUT_MS);
|
|
708
|
+
// Don't keep process alive just for this timer
|
|
709
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
710
|
+
timer.unref();
|
|
711
|
+
}
|
|
712
|
+
}),
|
|
713
|
+
]);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// Best-effort during cleanup
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Now safe to flush
|
|
720
|
+
sessionManager?.flushPendingToolResults?.();
|
|
721
|
+
}
|
|
722
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
723
|
+
// Session history sanitization (ported from OpenClaw)
|
|
724
|
+
// Pattern: openclaw/src/agents/session-transcript-repair.ts
|
|
725
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
726
|
+
/**
|
|
727
|
+
* Sanitize tool_use / tool_result pairing in session messages.
|
|
728
|
+
*
|
|
729
|
+
* Ensures every assistant tool_use block has a matching tool_result message
|
|
730
|
+
* immediately after it. This prevents API 400 errors from orphaned tool results,
|
|
731
|
+
* missing tool results, and duplicate tool results.
|
|
732
|
+
*
|
|
733
|
+
* Pattern: OpenClaw's sanitizeToolUseResultPairing
|
|
734
|
+
* (openclaw/src/agents/session-transcript-repair.ts)
|
|
735
|
+
*/
|
|
736
|
+
function sanitizeToolUseResultPairing(messages) {
|
|
737
|
+
if (!messages || messages.length === 0)
|
|
738
|
+
return messages;
|
|
739
|
+
const result = [];
|
|
740
|
+
for (let i = 0; i < messages.length; i++) {
|
|
741
|
+
const msg = messages[i];
|
|
742
|
+
result.push(msg);
|
|
743
|
+
// Only process assistant messages with tool_use content
|
|
744
|
+
if (msg.role !== "assistant")
|
|
745
|
+
continue;
|
|
746
|
+
const content = msg.content;
|
|
747
|
+
if (!Array.isArray(content))
|
|
748
|
+
continue;
|
|
749
|
+
// Collect tool_use IDs from this assistant message
|
|
750
|
+
const toolUseIds = new Set();
|
|
751
|
+
for (const block of content) {
|
|
752
|
+
if (block &&
|
|
753
|
+
typeof block === "object" &&
|
|
754
|
+
block.type === "tool_use" &&
|
|
755
|
+
typeof block.id === "string") {
|
|
756
|
+
toolUseIds.add(block.id);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (toolUseIds.size === 0)
|
|
760
|
+
continue;
|
|
761
|
+
// Skip repair for errored/aborted assistant messages (incomplete tool_use blocks)
|
|
762
|
+
const stopReason = msg.stop_reason ?? msg.stopReason;
|
|
763
|
+
if (stopReason === "error" || stopReason === "aborted")
|
|
764
|
+
continue;
|
|
765
|
+
// Collect tool_result messages that follow this assistant message
|
|
766
|
+
const seenResultIds = new Set();
|
|
767
|
+
let j = i + 1;
|
|
768
|
+
while (j < messages.length) {
|
|
769
|
+
const nextMsg = messages[j];
|
|
770
|
+
// Stop at the next assistant or user message
|
|
771
|
+
if (nextMsg.role === "assistant" || nextMsg.role === "user")
|
|
772
|
+
break;
|
|
773
|
+
// Process tool_result messages
|
|
774
|
+
if (nextMsg.role === "tool" || nextMsg.role === "toolResult") {
|
|
775
|
+
const toolUseId = nextMsg.tool_use_id ??
|
|
776
|
+
nextMsg.toolUseId;
|
|
777
|
+
if (typeof toolUseId === "string") {
|
|
778
|
+
if (!toolUseIds.has(toolUseId)) {
|
|
779
|
+
// Orphan tool_result — skip it (don't add to result)
|
|
780
|
+
j++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (seenResultIds.has(toolUseId)) {
|
|
784
|
+
// Duplicate tool_result — skip it
|
|
785
|
+
j++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
seenResultIds.add(toolUseId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
result.push(nextMsg);
|
|
792
|
+
j++;
|
|
793
|
+
}
|
|
794
|
+
// Insert synthetic error results for missing tool_use IDs
|
|
795
|
+
for (const id of toolUseIds) {
|
|
796
|
+
if (!seenResultIds.has(id)) {
|
|
797
|
+
result.push({
|
|
798
|
+
role: "tool",
|
|
799
|
+
tool_use_id: id,
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
802
|
+
type: "text",
|
|
803
|
+
text: JSON.stringify({
|
|
804
|
+
status: "error",
|
|
805
|
+
error: "Tool execution was interrupted",
|
|
806
|
+
}),
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
is_error: true,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Skip the tool_result messages we already processed
|
|
814
|
+
i = j - 1;
|
|
815
|
+
}
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Validate role alternation for Anthropic (user/assistant must alternate, starts with user).
|
|
820
|
+
* Merges consecutive same-role messages.
|
|
821
|
+
*
|
|
822
|
+
* Pattern: OpenClaw's validateAnthropicTurns
|
|
823
|
+
*/
|
|
824
|
+
function validateAnthropicTurns(messages) {
|
|
825
|
+
if (!messages || messages.length === 0)
|
|
826
|
+
return messages;
|
|
827
|
+
const result = [];
|
|
828
|
+
for (const msg of messages) {
|
|
829
|
+
const role = msg.role;
|
|
830
|
+
// Skip messages with roles that aren't user/assistant/tool
|
|
831
|
+
if (role !== "user" && role !== "assistant" && role !== "tool" && role !== "toolResult") {
|
|
832
|
+
result.push(msg);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const prev = result[result.length - 1];
|
|
836
|
+
const prevRole = prev?.role;
|
|
837
|
+
// Merge consecutive user messages
|
|
838
|
+
if (role === "user" && prevRole === "user") {
|
|
839
|
+
const prevContent = prev.content;
|
|
840
|
+
const curContent = msg.content;
|
|
841
|
+
if (typeof prevContent === "string" && typeof curContent === "string") {
|
|
842
|
+
prev.content = `${prevContent}\n\n${curContent}`;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
// Array-style content: merge arrays
|
|
846
|
+
const prevArr = Array.isArray(prevContent) ? prevContent : [{ type: "text", text: String(prevContent) }];
|
|
847
|
+
const curArr = Array.isArray(curContent) ? curContent : [{ type: "text", text: String(curContent) }];
|
|
848
|
+
prev.content = [...prevArr, ...curArr];
|
|
849
|
+
}
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
result.push(msg);
|
|
853
|
+
}
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Sanitize session messages before prompt submission.
|
|
858
|
+
* Runs tool_use/result pairing repair and role alternation validation.
|
|
859
|
+
*
|
|
860
|
+
* Pattern: OpenClaw's session sanitization pipeline (attempt.ts:664-695)
|
|
861
|
+
*/
|
|
862
|
+
export function sanitizeSessionMessages(messages) {
|
|
863
|
+
if (!messages || messages.length === 0)
|
|
864
|
+
return messages;
|
|
865
|
+
// 1. Repair tool_use/tool_result pairing (critical for API compatibility)
|
|
866
|
+
let sanitized = sanitizeToolUseResultPairing(messages);
|
|
867
|
+
// 2. Validate role alternation (Anthropic requires strict alternation)
|
|
868
|
+
sanitized = validateAnthropicTurns(sanitized);
|
|
869
|
+
return sanitized;
|
|
870
|
+
}
|
|
610
871
|
//# sourceMappingURL=run.js.map
|