@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.
Files changed (65) hide show
  1. package/dist/agent/agent.d.ts +105 -0
  2. package/dist/agent/agent.d.ts.map +1 -1
  3. package/dist/agent/agent.js +534 -0
  4. package/dist/agent/agent.js.map +1 -1
  5. package/dist/agent/model-fallback.d.ts.map +1 -1
  6. package/dist/agent/model-fallback.js +14 -0
  7. package/dist/agent/model-fallback.js.map +1 -1
  8. package/dist/agent/pi-embedded-runner/compact.d.ts +7 -0
  9. package/dist/agent/pi-embedded-runner/compact.d.ts.map +1 -1
  10. package/dist/agent/pi-embedded-runner/compact.js +100 -4
  11. package/dist/agent/pi-embedded-runner/compact.js.map +1 -1
  12. package/dist/agent/pi-embedded-runner/run.d.ts +7 -0
  13. package/dist/agent/pi-embedded-runner/run.d.ts.map +1 -1
  14. package/dist/agent/pi-embedded-runner/run.js +373 -112
  15. package/dist/agent/pi-embedded-runner/run.js.map +1 -1
  16. package/dist/agent/pi-embedded-runner/subscribe.d.ts +14 -0
  17. package/dist/agent/pi-embedded-runner/subscribe.d.ts.map +1 -1
  18. package/dist/agent/pi-embedded-runner/subscribe.js +54 -2
  19. package/dist/agent/pi-embedded-runner/subscribe.js.map +1 -1
  20. package/dist/agent/pi-embedded-runner/tool-result-context-guard.d.ts +33 -0
  21. package/dist/agent/pi-embedded-runner/tool-result-context-guard.d.ts.map +1 -0
  22. package/dist/agent/pi-embedded-runner/tool-result-context-guard.js +287 -0
  23. package/dist/agent/pi-embedded-runner/tool-result-context-guard.js.map +1 -0
  24. package/dist/agent/pi-embedded-runner/tools.d.ts +8 -1
  25. package/dist/agent/pi-embedded-runner/tools.d.ts.map +1 -1
  26. package/dist/agent/pi-embedded-runner/tools.js +10 -2
  27. package/dist/agent/pi-embedded-runner/tools.js.map +1 -1
  28. package/dist/agent/pi-embedded-runner/types.d.ts +11 -0
  29. package/dist/agent/pi-embedded-runner/types.d.ts.map +1 -1
  30. package/dist/api/chat.d.ts.map +1 -1
  31. package/dist/api/chat.js +13 -0
  32. package/dist/api/chat.js.map +1 -1
  33. package/dist/cli.js +1 -1
  34. package/dist/cron/index.d.ts +1 -1
  35. package/dist/cron/index.d.ts.map +1 -1
  36. package/dist/cron/schedule.d.ts +46 -0
  37. package/dist/cron/schedule.d.ts.map +1 -0
  38. package/dist/cron/schedule.js +109 -0
  39. package/dist/cron/schedule.js.map +1 -0
  40. package/dist/cron/scheduler.d.ts +86 -40
  41. package/dist/cron/scheduler.d.ts.map +1 -1
  42. package/dist/cron/scheduler.js +525 -159
  43. package/dist/cron/scheduler.js.map +1 -1
  44. package/dist/cron/types.d.ts +16 -0
  45. package/dist/cron/types.d.ts.map +1 -1
  46. package/dist/heartbeat/runner.d.ts +9 -4
  47. package/dist/heartbeat/runner.d.ts.map +1 -1
  48. package/dist/heartbeat/runner.js +116 -48
  49. package/dist/heartbeat/runner.js.map +1 -1
  50. package/dist/server.d.ts +1 -1
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +24 -3
  53. package/dist/server.js.map +1 -1
  54. package/dist/supabase/cron-jobs.d.ts.map +1 -1
  55. package/dist/supabase/cron-jobs.js +16 -6
  56. package/dist/supabase/cron-jobs.js.map +1 -1
  57. package/package.json +5 -4
  58. package/public/index.html +177 -56
  59. package/skills/okx/LEARNING.md +41 -0
  60. package/skills/okx/SKILL.md +231 -0
  61. package/skills/okx/references/api-reference.md +201 -0
  62. package/skills/okx/references/order-types.md +251 -0
  63. package/skills/okx/references/products.md +128 -0
  64. package/skills/okx/scripts/okx-trade.ts +273 -0
  65. 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
- const contextOverflowError = !aborted
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
- // Synthesize a context overflow error to trigger compaction
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} error=${errorText.slice(0, 200)}`);
138
+ `compactionAttempts=${overflowCompactionAttempts} ` +
139
+ `attemptCompactionCount=${attemptCompactionCount} error=${errorText.slice(0, 200)}`);
211
140
  const isCompactionFailure = isCompactionFailureError(errorText);
212
- // Attempt auto-compaction on context overflow (NOT on compaction_failure)
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}); attempting auto-compaction for ${provider}/${modelId}`);
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
- // Fallback: try truncating oversized tool results in the session.
239
- // This handles the case where a single tool result exceeds the
240
- // context window and compaction cannot reduce it further.
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); retrying prompt`);
260
- // Session is now smaller; allow compaction retries again.
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 message = describeUnknownError(promptError);
294
- console.error(`[PI_AGENT] Prompt error:`, message);
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
- console.log(`[PI_ATTEMPT] Loaded ${customTools.length} custom tools`);
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
- // Use the agent session to run the prompt
508
- await session.prompt(params.prompt, {
509
- images: piImages,
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
- sessionManager.flushPendingToolResults?.();
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