package com.aicodeassistant.engine;
import com.aicodeassistant.config.AgentTimeoutConfig;
import com.aicodeassistant.engine.correction.CorrectionInstruction;
import com.aicodeassistant.engine.correction.SelfCorrectionLoop;
import com.aicodeassistant.engine.scheduling.ToolPriorityScheduler;
import com.aicodeassistant.engine.strategy.DefaultTerminationStrategy;
import com.aicodeassistant.engine.strategy.TerminationDecision;
import com.aicodeassistant.engine.strategy.TerminationStrategy;
import com.aicodeassistant.engine.strategy.TerminationStrategy.LoopContext;
import com.aicodeassistant.engine.strategy.TerminationStrategy.ToolCallRecord;
import com.aicodeassistant.engine.tracking.ToolCallTracker;
import com.aicodeassistant.history.FileHistoryService;
import com.aicodeassistant.hook.HookRegistry;
import com.aicodeassistant.hook.HookService;
import com.aicodeassistant.llm.*;
import com.aicodeassistant.model.*;
import com.aicodeassistant.config.FeatureFlagService;
import com.aicodeassistant.tool.*;
import com.aicodeassistant.tool.agent.BackgroundAgentTracker;
import com.aicodeassistant.run.RunEnvelope;
import com.aicodeassistant.run.RunTracker;
import com.aicodeassistant.observability.MdcScope;
import com.aicodeassistant.observability.SafeLogValue;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
/**
* QueryEngine — 查询引擎核心循环。
*
* 8 步循环: 压缩检查 → 流式执行器初始化 → API 调用 → 流处理 →
* 工具执行 → 继续/终止判定 → 工具摘要注入 → 状态更新
*
* 在 Virtual Thread 中执行,配合 StreamChatCallback 实现流式输出。
*
*/
@Service
public class QueryEngine {
private static final Logger log = LoggerFactory.getLogger(QueryEngine.class);
private static final String MAX_TOKENS_RECOVERY_MESSAGE =
"Output token limit hit. Resume directly — no apology, " +
"no recap of what you were doing. Pick up mid-thought if " +
"that is where the cut happened. Break remaining work " +
"into smaller pieces.";
private static final String EMPTY_FINAL_RESPONSE_MESSAGE =
"Your previous response ended without a visible final answer. " +
"Please provide the final answer now.";
/**
* 系统内部折叠占位符独占整段正文 —— 仅当文本去除首尾空白后,
* 从头到尾只由这类占位符(可含中间空白)构成时才命中。
* 模型把工作集中见到的折叠占位符当作完整答复原样回传时,
* 视为"无可见正文"进入既有空正文恢复,而非判定成功答复。
* 窄域设计:占位符出现在段落中(如 INI 段名 [collapsed])不构成整串匹配,
* 用户合法内容一律保真。
*/
private static final Pattern SYSTEM_COLLAPSE_ONLY = Pattern.compile(
"\\s*(?:\\[(?:content compressed by system|content truncated by system"
+ "|collapsed|skeleton|summary-collapsed)\\]\\s*)+",
Pattern.CASE_INSENSITIVE);
private static final String TRUNCATED_SYSTEM_MARKER = "[content truncated by system]";
private static final String COMPRESSED_SYSTEM_MARKER = "[content compressed by system]";
private static final int MAX_RUN_INPUTS_PER_TURN = 10;
private enum LoopExit {
MODEL_FINISHED, MAX_TURNS, TOKEN_BUDGET_EXHAUSTED,
OUTPUT_RECOVERY_EXHAUSTED, USER_INPUT_REQUIRED, HOOK_STOPPED,
CONTEXT_RECOVERY_EXHAUSTED, CANCELLED, INTERNAL_ERROR
}
private record LoopOutcome(LoopExit reason, Usage usage, String finalMessageId) { }
private final LlmProviderRegistry providerRegistry;
private final CompactService compactService;
private final ApiRetryService apiRetryService;
private final TokenCounter tokenCounter;
private final ObjectMapper objectMapper;
private final StreamingToolExecutor streamingToolExecutor;
private final MessageNormalizer messageNormalizer;
private final HookService hookService;
private final SnipService snipService;
private final MicroCompactService microCompactService;
private final ModelRegistry modelRegistry; // P0-1 新增
private final ThinkingBudgetCalculator thinkingBudgetCalculator;
private final ModelTierService modelTierService;
private final FileHistoryService fileHistoryService;
private final ToolResultSummarizer toolResultSummarizer;
private final ContextCascade contextCascade;
private final CompactMetrics compactMetrics;
@org.springframework.lang.Nullable
private final IncrementalCollapseManager incrementalCollapseManager;
@org.springframework.lang.Nullable
private final VisualizationAutoRouter visualizationAutoRouter;
@org.springframework.lang.Nullable
private final BackgroundAgentTracker backgroundAgentTracker;
private final FeatureFlagService featureFlagService;
private final TerminationStrategy terminationStrategy;
private final ToolPriorityScheduler toolPriorityScheduler;
private final SelfCorrectionLoop selfCorrectionLoop;
private final AgentTimeoutConfig agentTimeoutConfig;
private final RunTracker runTracker;
private final TokenBudgetGuard tokenBudgetGuard;
private final ImageRefInjector imageRefInjector;
private final UserImageTranscoder userImageTranscoder;
private final com.aicodeassistant.run.RunExecutionRegistry runExecutions;
private volatile com.aicodeassistant.workbench.WorkbenchRunLinkService workbenchLinks;
/** 单条工具结果最大占上下文窗口的 30% */
private static final double TOOL_RESULT_BUDGET_RATIO = 0.3;
/** MicroCompact 保护尾部消息数 */
private static final int MICRO_COMPACT_PROTECTED_TAIL = 10;
/** 记录已通知的 thinking 降级 (once-only) */
private final Set notifiedThinkingDowngrades = ConcurrentHashMap.newKeySet();
/** 会话级中断上下文 — sessionId → AbortContext */
private final ConcurrentHashMap abortContexts = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupScheduler =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "abort-context-cleanup");
t.setDaemon(true);
return t;
});
private ScheduledFuture> cleanupTask;
public QueryEngine(LlmProviderRegistry providerRegistry,
CompactService compactService,
ApiRetryService apiRetryService,
TokenCounter tokenCounter,
ObjectMapper objectMapper,
StreamingToolExecutor streamingToolExecutor,
MessageNormalizer messageNormalizer,
HookService hookService,
SnipService snipService,
MicroCompactService microCompactService,
ModelRegistry modelRegistry,
ThinkingBudgetCalculator thinkingBudgetCalculator,
ModelTierService modelTierService,
FileHistoryService fileHistoryService,
ToolResultSummarizer toolResultSummarizer,
ContextCascade contextCascade,
CompactMetrics compactMetrics,
@org.springframework.lang.Nullable IncrementalCollapseManager incrementalCollapseManager,
@org.springframework.lang.Nullable VisualizationAutoRouter visualizationAutoRouter,
@org.springframework.lang.Nullable BackgroundAgentTracker backgroundAgentTracker,
FeatureFlagService featureFlagService,
TerminationStrategy terminationStrategy,
ToolPriorityScheduler toolPriorityScheduler,
SelfCorrectionLoop selfCorrectionLoop,
AgentTimeoutConfig agentTimeoutConfig,
TokenBudgetGuard tokenBudgetGuard,
ImageRefInjector imageRefInjector,
@org.springframework.context.annotation.Lazy RunTracker runTracker,
@org.springframework.lang.Nullable com.aicodeassistant.run.RunExecutionRegistry runExecutions,
UserImageTranscoder userImageTranscoder) {
this.providerRegistry = providerRegistry;
this.compactService = compactService;
this.apiRetryService = apiRetryService;
this.tokenCounter = tokenCounter;
this.objectMapper = objectMapper;
this.streamingToolExecutor = streamingToolExecutor;
this.messageNormalizer = messageNormalizer;
this.hookService = hookService;
this.snipService = snipService;
this.microCompactService = microCompactService;
this.modelRegistry = modelRegistry;
this.thinkingBudgetCalculator = thinkingBudgetCalculator;
this.modelTierService = modelTierService;
this.fileHistoryService = fileHistoryService;
this.toolResultSummarizer = toolResultSummarizer;
this.contextCascade = contextCascade;
this.compactMetrics = compactMetrics;
this.incrementalCollapseManager = incrementalCollapseManager;
this.visualizationAutoRouter = visualizationAutoRouter;
this.backgroundAgentTracker = backgroundAgentTracker;
this.featureFlagService = featureFlagService;
this.terminationStrategy = terminationStrategy;
this.toolPriorityScheduler = toolPriorityScheduler;
this.selfCorrectionLoop = selfCorrectionLoop;
this.agentTimeoutConfig = agentTimeoutConfig;
this.tokenBudgetGuard = tokenBudgetGuard;
this.imageRefInjector = imageRefInjector;
this.runTracker = runTracker;
this.runExecutions = runExecutions;
this.userImageTranscoder = userImageTranscoder;
}
@org.springframework.beans.factory.annotation.Autowired(required = false)
void setWorkbenchRunLinkService(
com.aicodeassistant.workbench.WorkbenchRunLinkService workbenchLinks) {
this.workbenchLinks = workbenchLinks;
}
@PostConstruct
void scheduleAbortContextCleanup() {
cleanupTask = cleanupScheduler.scheduleAtFixedRate(() -> {
int before = abortContexts.size();
abortContexts.entrySet().removeIf(e -> e.getValue().isExpired());
int removed = before - abortContexts.size();
if (removed > 0) {
log.info("AbortContext cleanup: removed {} expired entries, remaining {}",
removed, abortContexts.size());
}
}, 30, 30, TimeUnit.MINUTES);
}
@PreDestroy
void shutdownCleanupScheduler() {
if (cleanupTask != null) cleanupTask.cancel(false);
cleanupScheduler.shutdown();
}
/**
* 中断指定会话的查询循环。
* 由 WebSocketController.handleInterrupt() 调用。
*
* @param sessionId 会话 ID
* @param reason 中断原因
*/
public void abort(String sessionId, AbortReason reason) {
if (runExecutions != null && runExecutions.abortSession(sessionId, reason)) {
log.info("QueryEngine abort: sessionId={}, reason={}", sessionId, reason);
return;
}
AbortContext ctx = abortContexts.get(sessionId);
if (ctx != null) {
ctx.abort(reason);
log.info("QueryEngine abort: sessionId={}, reason={}", sessionId, reason);
} else {
// 连接断开时查询未启动/已结束属于正常场景,降为 debug 避免日志噪声
log.debug("No active AbortContext for sessionId={}", sessionId);
}
}
/**
* 获取或创建会话的 AbortContext。
*/
public AbortContext getOrCreateAbortContext(String sessionId) {
return abortContexts.computeIfAbsent(sessionId, k -> new AbortContext());
}
/**
* 移除会话的 AbortContext。
*/
public void removeAbortContext(String sessionId) {
abortContexts.remove(sessionId);
}
/**
* 获取会话的 AbortContext(只读,不创建)。
*/
public AbortContext getAbortContext(String sessionId) {
if (runExecutions != null) {
AbortContext registered = runExecutions.cancellationForSession(sessionId).orElse(null);
if (registered != null) return registered;
}
return abortContexts.get(sessionId);
}
/**
* 执行查询 — 查询引擎入口。
*
* 在 Virtual Thread 中运行完整的 8 步查询循环。
*
* @param config 查询配置
* @param state 循环状态
* @param handler 消息处理器 (流式输出)
* @return 查询结果
*/
public QueryResult execute(QueryConfig config, QueryLoopState state,
QueryMessageHandler handler) {
log.info("QueryEngine 开始执行: model={}, maxTokens={}, maxTurns={}",
config.model(), config.maxTokens(), config.maxTurns());
AtomicBoolean aborted = new AtomicBoolean(false);
Usage totalUsage = Usage.zero();
LoopOutcome loopOutcome = null;
// 将 AbortContext 连接到本地 aborted 标志,使得外部 abort() 调用能实际停止循环
String sessionId = state.getToolUseContext() != null
? state.getToolUseContext().sessionId() : null;
if (sessionId != null) {
AbortContext abortCtx = getOrCreateAbortContext(sessionId);
abortCtx.onAbort().thenAccept(reason -> {
state.setAbortReason(reason);
aborted.set(true);
});
}
// ★ RunTracker: 启动运行追踪并将 runId 传播到 ToolUseContext
String currentRunId = null;
boolean runFailureRecorded = false;
String parentRunId = state.getToolUseContext() != null
? state.getToolUseContext().currentRunId() : null;
String agentType = state.getToolUseContext() != null
&& state.getToolUseContext().parentSessionId() != null ? "subagent" : "query";
if (runTracker != null && sessionId != null) {
try {
RunEnvelope run = runTracker.startRun(sessionId, parentRunId, agentType, config.model());
currentRunId = run.id();
if (runExecutions != null) {
runExecutions.register(currentRunId, sessionId, getOrCreateAbortContext(sessionId));
}
if (state.getToolUseContext() != null) {
state.setToolUseContext(state.getToolUseContext().withCurrentRunId(currentRunId));
}
if (parentRunId == null && workbenchLinks != null) {
state.getMessages().stream()
.filter(Message.UserMessage.class::isInstance)
.map(Message.UserMessage.class::cast)
.reduce((first, second) -> second)
.ifPresent(request -> workbenchLinks.bindRequest(run.id(), request));
}
} catch (Exception e) {
log.error("Failed to establish Run execution authority", e);
if (currentRunId != null && runTracker != null) {
try { runTracker.failRun(currentRunId, "RUN_EXECUTION_REGISTRATION_FAILED"); }
catch (Exception recordFailure) {
log.error("Failed to terminate partially-created Run {}", currentRunId, recordFailure);
}
}
unregisterRunExecution(currentRunId);
handler.onError(e);
return new QueryResult(state.getMessages(), totalUsage,
"error", "RUN_EXECUTION_REGISTRATION_FAILED", state.getTurnCount());
}
}
Map runCorrelation = new LinkedHashMap<>();
if (sessionId != null) runCorrelation.put("sessionId", sessionId);
if (currentRunId != null) runCorrelation.put("runId", currentRunId);
if (parentRunId != null) runCorrelation.put("parentRunId", parentRunId);
runCorrelation.put("agentType", agentType);
try (MdcScope ignoredRunScope = MdcScope.open(runCorrelation)) {
try {
if (backgroundAgentTracker != null) backgroundAgentTracker.retainRun(currentRunId);
preCleanImageHistory(config, state);
loopOutcome = queryLoop(config, state, handler, aborted);
totalUsage = loopOutcome.usage();
} catch (HandoffContextService.CapacityException e) {
// Only merged sessions can raise this. Compaction cannot shrink the mandatory entry.
state.setRecoveryExhausted(true);
state.setRecoveryFailureMessage(e.getMessage());
totalUsage = state.getObservedUsage();
loopOutcome = new LoopOutcome(LoopExit.CONTEXT_RECOVERY_EXHAUSTED, totalUsage, null);
handler.onError(e);
} catch (Exception e) {
boolean persistenceFailed = e instanceof com.aicodeassistant.session.MessagePersistenceException;
boolean cancelled = !persistenceFailed && (aborted.get() || isCancellation(e));
if (cancelled && state.getAbortReason() == null) state.setAbortReason(AbortReason.USER_INTERRUPT);
if (cancelled) log.info("QueryEngine execution cancelled: {}", e.getMessage());
else {
log.error("QueryEngine 执行异常", e);
}
// Provider HTTP 错误(402/403/429 等)分类后写入 QueryResult.error,
// 保证子代理/父链路消费 result.error() 时能拿到结构化错误码
com.aicodeassistant.llm.ProviderErrorClassifier.ClassifiedError classified =
com.aicodeassistant.llm.ProviderErrorClassifier.classify(e);
String errorDetail = e instanceof com.aicodeassistant.session.MessagePersistenceException persistenceFailure
? persistenceFailureDetail(persistenceFailure)
: classified != null
? classified.errorCode() + ": " + classified.message()
: e.getMessage();
rejectPendingRunInputs(
currentRunId,
state.getTurnCount() >= config.maxTurns()
? "TURN_LIMIT_REACHED"
: "RUN_NOT_ACCEPTING_INPUT",
handler);
// ★ RunTracker: 异常路径 — 标记为 FAILED
if (currentRunId != null && runTracker != null) {
try {
if (cancelled) {
AbortReason reason = state.getAbortReason() != null
? state.getAbortReason() : AbortReason.USER_INTERRUPT;
runTracker.abortRun(currentRunId, reason, cancellationDetail(reason));
}
else if (e instanceof com.aicodeassistant.session.MessagePersistenceException) {
runTracker.failRun(currentRunId, RunEnvelope.RunExitReason.INCOMPLETE, errorDetail);
} else runTracker.failRun(currentRunId, errorDetail);
runFailureRecorded = true;
} catch (Exception ex) {
log.warn("Failed to record RunTracker failure: {}", ex.getMessage());
}
}
unregisterRunExecution(currentRunId);
QueryResult actual = projectTerminalResult(new QueryResult(state.getMessages(), state.getObservedUsage(),
"error", errorDetail, state.getTurnCount()), currentRunId, state.getAbortReason(), handler, true);
if ("error".equals(actual.stopReason())) {
handler.onError(Objects.equals(actual.error(), errorDetail) ? e : new IllegalStateException(actual.error(), e));
}
return actual;
} finally {
if (backgroundAgentTracker != null) backgroundAgentTracker.releaseRun(currentRunId);
// P1-04: 确保清理 AbortContext,防止内存泄漏
if (sessionId != null) {
abortContexts.remove(sessionId);
}
}
rejectPendingRunInputs(
currentRunId,
state.getTurnCount() >= config.maxTurns()
? "TURN_LIMIT_REACHED"
: "RUN_NOT_ACCEPTING_INPUT",
handler);
// Terminal persistence closes local Run execution as a side effect. Preserve
// whether cancellation existed before that transition so it cannot overwrite
// the query loop's real exit reason (for example MAX_TURNS).
boolean abortedBeforeTerminalTransition = aborted.get();
// ★ RunTracker: 根据实际结束原因选择正确的状态转换
if (!runFailureRecorded && currentRunId != null && runTracker != null) {
try {
if (abortedBeforeTerminalTransition) {
// 用户中断或超时 — 标记为 ABORTED
AbortReason abortReason = state.getAbortReason() != null
? state.getAbortReason() : AbortReason.USER_INTERRUPT;
runTracker.abortRun(currentRunId, abortReason, cancellationDetail(abortReason));
} else if (loopOutcome == null || loopOutcome.reason() != LoopExit.MODEL_FINISHED) {
String detail = loopOutcome == null
? "INTERNAL_ERROR: query loop returned no outcome"
: loopError(loopOutcome.reason(), state);
runTracker.failRun(currentRunId, RunEnvelope.RunExitReason.INCOMPLETE, detail);
} else {
// 正常完成 — 标记为 COMPLETED
runTracker.completeRun(currentRunId, totalUsage.totalTokens(),
0.0, 0, state.getTurnCount());
}
} catch (Exception e) {
log.warn("Failed to update RunTracker run status: {}", e.getMessage());
}
}
unregisterRunExecution(currentRunId);
LoopExit actualExit = abortedBeforeTerminalTransition ? LoopExit.CANCELLED
: loopOutcome == null ? LoopExit.INTERNAL_ERROR : loopOutcome.reason();
String stopReason = actualExit == LoopExit.MODEL_FINISHED ? "end_turn"
: actualExit == LoopExit.MAX_TURNS ? "max_turns" : "error";
String error = actualExit == LoopExit.MODEL_FINISHED ? null : loopError(actualExit, state);
log.info("QueryEngine 完成: turns={}, stopReason={}, totalTokens={}",
state.getTurnCount(), stopReason, totalUsage.totalTokens());
// CONTEXT_RECOVERY_EXHAUSTED 的三个循环出口(本地预算守卫、最终 payload 413、
// 413 恢复耗尽)已在循环内向 handler 发布过真实异常(LlmApiException 等)。
// 此处传 errorAlreadyPublished=true,避免 projectTerminalResult 再用包装的
// IllegalStateException 重发一次,导致前端重复报错并丢失 413 状态码/原始异常类型。
boolean errorAlreadyPublished = actualExit == LoopExit.CONTEXT_RECOVERY_EXHAUSTED;
return projectTerminalResult(new QueryResult(state.getMessages(), totalUsage,
stopReason, error, state.getTurnCount()), currentRunId, state.getAbortReason(),
handler, errorAlreadyPublished);
}
}
private static String loopError(LoopExit exit, QueryLoopState state) {
if (exit == LoopExit.CONTEXT_RECOVERY_EXHAUSTED
&& state != null && state.getRecoveryFailureMessage() != null) {
return "CONTEXT_RECOVERY_EXHAUSTED: " + state.getRecoveryFailureMessage();
}
return switch (exit) {
case MAX_TURNS -> "MAX_TURNS: maximum turn count reached";
case TOKEN_BUDGET_EXHAUSTED -> "TOKEN_BUDGET_EXHAUSTED: token budget exhausted";
case OUTPUT_RECOVERY_EXHAUSTED -> "OUTPUT_RECOVERY_EXHAUSTED: final output could not be completed";
case USER_INPUT_REQUIRED -> "USER_INPUT_REQUIRED: user guidance is required";
case HOOK_STOPPED -> "HOOK_STOPPED: stop hook prevented completion";
case CONTEXT_RECOVERY_EXHAUSTED -> "CONTEXT_RECOVERY_EXHAUSTED: context recovery exhausted";
case CANCELLED -> "CANCELLED: execution aborted";
case INTERNAL_ERROR -> "INTERNAL_ERROR: query loop exited unexpectedly";
case MODEL_FINISHED -> null;
};
}
private QueryResult projectTerminalResult(QueryResult proposed, String runId,
AbortReason requested, QueryMessageHandler handler) {
return projectTerminalResult(proposed, runId, requested, handler, false);
}
private QueryResult projectTerminalResult(QueryResult proposed, String runId,
AbortReason requested, QueryMessageHandler handler, boolean errorAlreadyPublished) {
QueryResult actual = RunResultProjection.resolve(proposed, runTracker, runId, requested);
if (!errorAlreadyPublished && actual.error() != null && "error".equals(actual.stopReason())) {
handler.onError(new IllegalStateException(actual.error()));
}
return actual;
}
private void unregisterRunExecution(String runId) {
if (runId == null || runExecutions == null) return;
try { runExecutions.unregister(runId); }
catch (Exception e) { log.error("Run execution cleanup failed: run={}", runId, e); }
}
private static String currentRunId(QueryLoopState state) {
return state.getToolUseContext() == null
? null : state.getToolUseContext().currentRunId();
}
private int applyRunInputs(
List
applications,
QueryLoopState state,
QueryMessageHandler handler) {
int appliedCount = 0;
for (int index = 0; index < applications.size(); index++) {
var application = applications.get(index);
com.aicodeassistant.run.RunExecutionRegistry.InputReceipt
appliedReceipt = null;
com.aicodeassistant.run.RunExecutionRegistry.InputReceipt
rejectedReceipt = null;
try {
var input = application.input();
var receipt = application.applyIfAccepting(
System.currentTimeMillis(),
() -> state.addMessage(new Message.UserMessage(
input.requestId(), Instant.now(),
List.of(new ContentBlock.TextBlock(
input.text())),
null, null,
input.meta() == null || input.meta().isEmpty()
? null : input.meta())));
if (receipt.state()
== com.aicodeassistant.run.RunExecutionRegistry
.InputState.APPLIED) {
appliedReceipt = receipt;
appliedCount++;
} else {
rejectedReceipt = receipt;
}
} catch (RuntimeException applyFailure) {
try {
rejectedReceipt = application.reject(
applyFailure instanceof com.aicodeassistant.session.MessagePersistenceException
? "APPLY_UNCONFIRMED" : "APPLY_FAILED");
} catch (RuntimeException settleFailure) {
applyFailure.addSuppressed(settleFailure);
}
log.error("Failed to apply queued run input: requestId={}",
application.input().requestId(), applyFailure);
if (applyFailure instanceof com.aicodeassistant.session.MessagePersistenceException) {
if (rejectedReceipt != null) emitRunInputRejected(handler, rejectedReceipt);
for (int remaining = index + 1; remaining < applications.size(); remaining++) {
var pending = applications.get(remaining);
try {
emitRunInputRejected(handler, pending.reject("APPLY_FAILED"));
} catch (RuntimeException pendingFailure) {
applyFailure.addSuppressed(pendingFailure);
} finally {
try {
pending.close();
} catch (RuntimeException closeFailure) {
applyFailure.addSuppressed(closeFailure);
}
}
}
throw applyFailure;
}
} finally {
application.close();
}
if (appliedReceipt != null) {
emitRunInputApplied(handler, appliedReceipt);
} else if (rejectedReceipt != null) {
emitRunInputRejected(handler, rejectedReceipt);
}
}
return appliedCount;
}
private void rejectPendingRunInputs(
String runId, String rejectionCode,
QueryMessageHandler handler) {
if (runExecutions == null || runId == null) return;
runExecutions.sealAndRejectInputs(runId, rejectionCode)
.forEach(receipt ->
emitRunInputRejected(handler, receipt));
}
private void emitRunInputApplied(
QueryMessageHandler handler,
com.aicodeassistant.run.RunExecutionRegistry.InputReceipt receipt) {
recordCurrentRunEvent("run_input_applied", Map.of(
"requestId", receipt.requestId(),
"textLength", SafeLogValue.length(receipt.text()),
"textFingerprint", SafeLogValue.fingerprint(receipt.text()),
"appliedAt", receipt.appliedAt()));
try {
handler.onStreamEvent("run_input_applied", Map.of(
"requestId", receipt.requestId(),
"text", receipt.text(),
"appliedAt", receipt.appliedAt()));
} catch (RuntimeException deliveryFailure) {
log.warn("Failed to deliver run_input_applied: requestId={}, error={}",
receipt.requestId(), deliveryFailure.getMessage());
}
}
private void emitRunInputRejected(
QueryMessageHandler handler,
com.aicodeassistant.run.RunExecutionRegistry.InputReceipt receipt) {
recordCurrentRunEvent("run_input_rejected", Map.of(
"requestId", receipt.requestId(),
"rejectionCode", receipt.rejectionCode() == null ? "unknown" : receipt.rejectionCode(),
"textLength", SafeLogValue.length(receipt.text()),
"textFingerprint", SafeLogValue.fingerprint(receipt.text()),
"rejectedAt", receipt.rejectedAt()));
try {
handler.onStreamEvent("run_input_rejected", Map.of(
"requestId", receipt.requestId(),
"code", receipt.rejectionCode(),
"message", receipt.rejectionMessage(),
"rejectedAt", receipt.rejectedAt()));
} catch (RuntimeException deliveryFailure) {
log.warn("Failed to deliver run_input_rejected: requestId={}, error={}",
receipt.requestId(), deliveryFailure.getMessage());
}
}
private void recordCurrentRunEvent(String type, Map data) {
recordCurrentRunEvent(type, () -> data);
}
private void recordCurrentRunEvent(
String type, java.util.function.Supplier