mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
refactor(agents): extract attempt session boundaries
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildTimestampPrefix } from "../../../gateway/server-methods/agent-timestamp.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import type { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
|
||||
import type { AgentSession } from "../../sessions/index.js";
|
||||
import { prepareEmbeddedAttemptSessionBoundary } from "./attempt-session-boundary.js";
|
||||
|
||||
function createActiveSession(messages: AgentMessage[] = []) {
|
||||
const reset = vi.fn();
|
||||
const convertToLlm = vi.fn((input: AgentMessage[]) => input as never);
|
||||
const activeSession = {
|
||||
agent: {
|
||||
reset,
|
||||
state: { messages },
|
||||
convertToLlm,
|
||||
},
|
||||
} as unknown as Pick<AgentSession, "agent">;
|
||||
return { activeSession, convertToLlm, reset };
|
||||
}
|
||||
|
||||
function createSessionManager(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): ReturnType<typeof guardSessionManager> {
|
||||
return {
|
||||
getLeafEntry: () => undefined,
|
||||
...overrides,
|
||||
} as unknown as ReturnType<typeof guardSessionManager>;
|
||||
}
|
||||
|
||||
describe("prepareEmbeddedAttemptSessionBoundary", () => {
|
||||
it("resets restored state and preserves exact prompt bytes for raw model probes", async () => {
|
||||
const { activeSession, reset } = createActiveSession();
|
||||
const setActiveSessionSystemPrompt = vi.fn();
|
||||
|
||||
const boundary = prepareEmbeddedAttemptSessionBoundary({
|
||||
activeSession,
|
||||
attempt: { prompt: "exact probe" },
|
||||
isRawModelRun: true,
|
||||
preparedUserTurnMessage: undefined,
|
||||
sessionManager: createSessionManager(),
|
||||
setActiveSessionSystemPrompt,
|
||||
});
|
||||
const converted = await activeSession.agent.convertToLlm([
|
||||
{ role: "user", content: [{ type: "text", text: "exact probe" }], timestamp: 1 },
|
||||
]);
|
||||
|
||||
expect(reset).toHaveBeenCalledOnce();
|
||||
expect(setActiveSessionSystemPrompt).toHaveBeenCalledWith("");
|
||||
expect(boundary).toMatchObject({
|
||||
boundaryTimezone: undefined,
|
||||
includeBoundaryTimestamp: false,
|
||||
orphanRepair: undefined,
|
||||
});
|
||||
expect((converted[0] as { content?: unknown }).content).toBe("exact probe");
|
||||
});
|
||||
|
||||
it("applies the prepared current-turn timestamp at the LLM boundary", async () => {
|
||||
const { activeSession } = createActiveSession();
|
||||
const preparedTimestamp = 1_717_570_800_000;
|
||||
const boundary = prepareEmbeddedAttemptSessionBoundary({
|
||||
activeSession,
|
||||
attempt: {
|
||||
config: { agents: { defaults: { userTimezone: "UTC" } } },
|
||||
prompt: "Current ask",
|
||||
trigger: "user",
|
||||
},
|
||||
isRawModelRun: false,
|
||||
preparedUserTurnMessage: undefined,
|
||||
sessionManager: createSessionManager(),
|
||||
setActiveSessionSystemPrompt: vi.fn(),
|
||||
});
|
||||
boundary.setCurrentUserTimestampOverride({
|
||||
timestamp: preparedTimestamp,
|
||||
text: "Current ask",
|
||||
});
|
||||
|
||||
const converted = await activeSession.agent.convertToLlm([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Current ask" }],
|
||||
timestamp: preparedTimestamp + 60_000,
|
||||
},
|
||||
]);
|
||||
|
||||
expect((converted[0] as { content?: unknown }).content).toBe(
|
||||
`${buildTimestampPrefix(new Date(preparedTimestamp), { timezone: "UTC" })}Current ask`,
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs an orphaned user leaf before rebuilding active session messages", () => {
|
||||
const repairedMessages: AgentMessage[] = [
|
||||
{ role: "user", content: [{ type: "text", text: "repaired" }], timestamp: 2 },
|
||||
];
|
||||
const { activeSession } = createActiveSession([
|
||||
{ role: "user", content: [{ type: "text", text: "old" }], timestamp: 1 },
|
||||
]);
|
||||
const branch = vi.fn();
|
||||
const clearNextUserMessagePersistenceSuppression = vi.fn();
|
||||
const onUserMessagePersistenceInvalidated = vi.fn();
|
||||
const sessionManager = createSessionManager({
|
||||
getLeafEntry: () => ({
|
||||
id: "user-leaf",
|
||||
parentId: "parent-entry",
|
||||
type: "message",
|
||||
timestamp: "2026-07-13T00:00:00.000Z",
|
||||
message: { role: "user", content: "old" },
|
||||
}),
|
||||
branch,
|
||||
clearNextUserMessagePersistenceSuppression,
|
||||
buildSessionContext: () => ({ messages: repairedMessages }),
|
||||
});
|
||||
|
||||
const boundary = prepareEmbeddedAttemptSessionBoundary({
|
||||
activeSession,
|
||||
attempt: {
|
||||
onUserMessagePersistenceInvalidated,
|
||||
prompt: "new",
|
||||
trigger: "user",
|
||||
},
|
||||
isRawModelRun: false,
|
||||
preparedUserTurnMessage: undefined,
|
||||
sessionManager,
|
||||
setActiveSessionSystemPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
expect(boundary.orphanRepair?.removeLeaf).toBe(true);
|
||||
expect(branch).toHaveBeenCalledWith("parent-entry");
|
||||
expect(clearNextUserMessagePersistenceSuppression).toHaveBeenCalledOnce();
|
||||
expect(onUserMessagePersistenceInvalidated).toHaveBeenCalledOnce();
|
||||
expect(activeSession.agent.state.messages).toBe(repairedMessages);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/** Prepares the restored transcript at the LLM boundary for one attempt. */
|
||||
import { resolveUserTimezone } from "../../date-time.js";
|
||||
import { relocateCurrentRuntimeContextCarrierToTail } from "../../internal-runtime-context.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import type { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
|
||||
import type { AgentSession } from "../../sessions/index.js";
|
||||
import {
|
||||
replayTrailingEntriesForOrphanRepair,
|
||||
resolveOrphanRepairPlan,
|
||||
} from "./attempt-orphan-repair.js";
|
||||
import { normalizeMessagesForLlmBoundary } from "./attempt.llm-boundary.js";
|
||||
import { detachPrePersistedCurrentUserTurn } from "./pre-persisted-user-turn.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
||||
type SessionBoundaryAttempt = Pick<
|
||||
EmbeddedRunAttemptParams,
|
||||
| "config"
|
||||
| "onUserMessagePersistenceInvalidated"
|
||||
| "prompt"
|
||||
| "suppressNextUserMessagePersistence"
|
||||
| "trigger"
|
||||
| "userTurnTranscriptRecorder"
|
||||
>;
|
||||
|
||||
type LlmBoundaryOptions = NonNullable<Parameters<typeof normalizeMessagesForLlmBoundary>[1]>;
|
||||
|
||||
export type CurrentUserTimestampOverride = NonNullable<
|
||||
LlmBoundaryOptions["currentUserTimestampOverride"]
|
||||
>;
|
||||
|
||||
export function prepareEmbeddedAttemptSessionBoundary(input: {
|
||||
activeSession: Pick<AgentSession, "agent">;
|
||||
attempt: SessionBoundaryAttempt;
|
||||
isRawModelRun: boolean;
|
||||
preparedUserTurnMessage: AgentMessage | undefined;
|
||||
sessionManager: ReturnType<typeof guardSessionManager>;
|
||||
setActiveSessionSystemPrompt: (systemPrompt: string) => void;
|
||||
}): {
|
||||
boundaryTimezone: string | undefined;
|
||||
includeBoundaryTimestamp: boolean;
|
||||
orphanRepair: ReturnType<typeof resolveOrphanRepairPlan>;
|
||||
setCurrentUserTimestampOverride: (override: CurrentUserTimestampOverride | undefined) => void;
|
||||
} {
|
||||
const { activeSession, attempt, isRawModelRun, sessionManager } = input;
|
||||
if (isRawModelRun) {
|
||||
// Raw probes measure only the requested provider prompt. Restored history,
|
||||
// queued work, and the normal system prompt would contaminate it.
|
||||
activeSession.agent.reset();
|
||||
input.setActiveSessionSystemPrompt("");
|
||||
}
|
||||
|
||||
const orphanRepair = isRawModelRun
|
||||
? undefined
|
||||
: resolveOrphanRepairPlan({
|
||||
sessionManager,
|
||||
prompt: attempt.prompt,
|
||||
trigger: attempt.trigger,
|
||||
});
|
||||
if (orphanRepair?.removeLeaf) {
|
||||
if (orphanRepair.messageEntry.parentId) {
|
||||
sessionManager.branch(orphanRepair.messageEntry.parentId);
|
||||
} else {
|
||||
sessionManager.resetLeaf();
|
||||
}
|
||||
replayTrailingEntriesForOrphanRepair(sessionManager, orphanRepair.trailingEntries);
|
||||
// The old canonical user turn is gone. Its persistence suppression must not
|
||||
// discard the merged replacement prompt.
|
||||
sessionManager.clearNextUserMessagePersistenceSuppression?.();
|
||||
attempt.onUserMessagePersistenceInvalidated?.();
|
||||
activeSession.agent.state.messages = sessionManager.buildSessionContext().messages;
|
||||
}
|
||||
|
||||
detachPrePersistedCurrentUserTurn({
|
||||
activeSession,
|
||||
preparedUserTurnMessage: input.preparedUserTurnMessage,
|
||||
suppressNextUserMessagePersistence: attempt.suppressNextUserMessagePersistence,
|
||||
userTurnAlreadyPersisted: attempt.userTurnTranscriptRecorder?.hasPersisted() === true,
|
||||
});
|
||||
|
||||
// This is the single timestamping source for user messages sent to the LLM.
|
||||
// Raw probes retain exact prompt bytes.
|
||||
const boundaryTimezone = isRawModelRun
|
||||
? undefined
|
||||
: resolveUserTimezone(attempt.config?.agents?.defaults?.userTimezone);
|
||||
const includeBoundaryTimestamp =
|
||||
!isRawModelRun && attempt.config?.agents?.defaults?.envelopeTimestamp !== "off";
|
||||
let currentUserTimestampOverride: CurrentUserTimestampOverride | undefined;
|
||||
const buildBoundaryOptions = (): LlmBoundaryOptions | undefined => {
|
||||
if (isRawModelRun) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(boundaryTimezone ? { timezone: boundaryTimezone } : {}),
|
||||
...(includeBoundaryTimestamp ? {} : { includeTimestamp: false }),
|
||||
...(currentUserTimestampOverride ? { currentUserTimestampOverride } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
if (typeof activeSession.agent.convertToLlm === "function") {
|
||||
const baseConvertToLlm = activeSession.agent.convertToLlm.bind(activeSession.agent);
|
||||
activeSession.agent.convertToLlm = async (messages) =>
|
||||
await baseConvertToLlm(
|
||||
// Wire-only relocation keeps the request append-only through the active
|
||||
// user turn without changing position-sensitive precheck normalization.
|
||||
relocateCurrentRuntimeContextCarrierToTail(
|
||||
normalizeMessagesForLlmBoundary(messages, buildBoundaryOptions()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
boundaryTimezone,
|
||||
includeBoundaryTimestamp,
|
||||
orphanRepair,
|
||||
setCurrentUserTimestampOverride: (override) => {
|
||||
currentUserTimestampOverride = override;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AgentSession } from "../../sessions/index.js";
|
||||
import { createEmbeddedAttemptSessionSettleTracker } from "./attempt-session-settle.js";
|
||||
|
||||
function deferred(): { promise: Promise<void>; resolve: () => void } {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("createEmbeddedAttemptSessionSettleTracker", () => {
|
||||
it("waits for both prompt and abort settlement during cleanup", async () => {
|
||||
const prompt = deferred();
|
||||
const abort = deferred();
|
||||
const abortSession = vi.fn(() => abort.promise);
|
||||
const tracker = createEmbeddedAttemptSessionSettleTracker({
|
||||
abort: abortSession,
|
||||
} as unknown as Pick<AgentSession, "abort">);
|
||||
|
||||
void tracker.trackPromptSettlePromise(prompt.promise);
|
||||
const abortReason = new Error("stop");
|
||||
void tracker.abortActiveSession(abortReason);
|
||||
const settled = tracker.buildAbortSettlePromise();
|
||||
expect(settled).not.toBeNull();
|
||||
expect(abortSession).toHaveBeenCalledWith(abortReason);
|
||||
|
||||
let finished = false;
|
||||
void settled?.then(() => {
|
||||
finished = true;
|
||||
});
|
||||
prompt.resolve();
|
||||
await Promise.resolve();
|
||||
expect(finished).toBe(false);
|
||||
|
||||
abort.resolve();
|
||||
await settled;
|
||||
expect(finished).toBe(true);
|
||||
expect(tracker.buildAbortSettlePromise()).toBeNull();
|
||||
});
|
||||
|
||||
it("settles rejected prompt work without leaking it into later cleanup", async () => {
|
||||
const tracker = createEmbeddedAttemptSessionSettleTracker({
|
||||
abort: async () => undefined,
|
||||
} as unknown as Pick<AgentSession, "abort">);
|
||||
void tracker.trackPromptSettlePromise(Promise.reject(new Error("prompt failed")));
|
||||
|
||||
await expect(tracker.buildAbortSettlePromise()).resolves.toBeUndefined();
|
||||
expect(tracker.buildAbortSettlePromise()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Tracks native prompt and abort settlement through attempt cleanup. */
|
||||
import type { AgentSession } from "../../sessions/index.js";
|
||||
|
||||
export function createEmbeddedAttemptSessionSettleTracker(
|
||||
activeSession: Pick<AgentSession, "abort">,
|
||||
): {
|
||||
abortActiveSession: (reason?: unknown) => Promise<void>;
|
||||
buildAbortSettlePromise: () => Promise<void> | null;
|
||||
trackPromptSettlePromise: (promise: Promise<void>) => Promise<void>;
|
||||
} {
|
||||
const inFlightPromptSettlePromises = new Set<Promise<void>>();
|
||||
const inFlightAbortSettlePromises = new Set<Promise<void>>();
|
||||
const trackSettlePromise = (
|
||||
promises: Set<Promise<void>>,
|
||||
promise: Promise<void>,
|
||||
): Promise<void> => {
|
||||
promises.add(promise);
|
||||
void promise.then(
|
||||
() => {
|
||||
promises.delete(promise);
|
||||
},
|
||||
() => {
|
||||
promises.delete(promise);
|
||||
},
|
||||
);
|
||||
return promise;
|
||||
};
|
||||
|
||||
const trackPromptSettlePromise = (promise: Promise<void>): Promise<void> =>
|
||||
trackSettlePromise(inFlightPromptSettlePromises, promise);
|
||||
const abortActiveSession = (reason?: unknown): Promise<void> =>
|
||||
trackSettlePromise(inFlightAbortSettlePromises, Promise.resolve(activeSession.abort(reason)));
|
||||
const buildAbortSettlePromise = (): Promise<void> | null => {
|
||||
const promises = [...inFlightPromptSettlePromises, ...inFlightAbortSettlePromises];
|
||||
return promises.length === 0 ? null : Promise.allSettled(promises).then(() => undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
abortActiveSession,
|
||||
buildAbortSettlePromise,
|
||||
trackPromptSettlePromise,
|
||||
};
|
||||
}
|
||||
@@ -36,12 +36,10 @@ import { resolveAgentDir, resolveSessionAgentIds } from "../../agent-scope.js";
|
||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||
import { isHeartbeatLifecycleRunKind } from "../../bootstrap-mode.js";
|
||||
import { createCacheTrace } from "../../cache-trace.js";
|
||||
import { resolveUserTimezone } from "../../date-time.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js";
|
||||
import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js";
|
||||
import { isSignalTimeoutReason } from "../../failover-error.js";
|
||||
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import { relocateCurrentRuntimeContextCarrierToTail } from "../../internal-runtime-context.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import type { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
|
||||
import { acquireSessionWriteLock } from "../../session-write-lock.js";
|
||||
@@ -80,10 +78,6 @@ import { prepareEmbeddedAttemptBootstrap } from "./attempt-bootstrap-prepare.js"
|
||||
import { prepareEmbeddedAttemptBundleTools } from "./attempt-bundle-tools.js";
|
||||
import { summarizeSessionContext } from "./attempt-context-summary.js";
|
||||
import { prepareEmbeddedAttemptHistory } from "./attempt-history-prepare.js";
|
||||
import {
|
||||
replayTrailingEntriesForOrphanRepair,
|
||||
resolveOrphanRepairPlan,
|
||||
} from "./attempt-orphan-repair.js";
|
||||
import { prepareEmbeddedAttemptPromptAssembly } from "./attempt-prompt-assembly.js";
|
||||
import { prepareEmbeddedAttemptPromptContext } from "./attempt-prompt-context.js";
|
||||
import {
|
||||
@@ -92,8 +86,10 @@ import {
|
||||
} from "./attempt-prompt-preflight.js";
|
||||
import { submitEmbeddedAttemptPrompt } from "./attempt-prompt-submit.js";
|
||||
import { completeEmbeddedAttemptResult } from "./attempt-result.js";
|
||||
import { prepareEmbeddedAttemptSessionBoundary } from "./attempt-session-boundary.js";
|
||||
import { cleanupEmbeddedAttemptSessionPhase } from "./attempt-session-cleanup.js";
|
||||
import { prepareEmbeddedAttemptSessionManager } from "./attempt-session-manager-prepare.js";
|
||||
import { createEmbeddedAttemptSessionSettleTracker } from "./attempt-session-settle.js";
|
||||
import { prepareEmbeddedAttemptAgentSession } from "./attempt-session.js";
|
||||
import { prepareEmbeddedAttemptSetup } from "./attempt-setup.js";
|
||||
import { createEmbeddedRunStageTracker } from "./attempt-stage-timing.js";
|
||||
@@ -117,7 +113,6 @@ import {
|
||||
resolveAttemptTrajectorySessionFile,
|
||||
} from "./attempt-transcript-helpers.js";
|
||||
import { buildLoopPromptCacheInfo } from "./attempt.context-engine-helpers.js";
|
||||
import { normalizeMessagesForLlmBoundary } from "./attempt.llm-boundary.js";
|
||||
import {
|
||||
buildAfterTurnRuntimeContext,
|
||||
resolvePromptSubmissionSkipReason,
|
||||
@@ -141,7 +136,6 @@ import { shouldFlagCompactionTimeout } from "./compaction-timeout.js";
|
||||
import { installHistoryImagePruneContextTransform } from "./history-image-prune.js";
|
||||
import { detectAndLoadPromptImages } from "./images.js";
|
||||
import { isMidTurnPrecheckSignal, type MidTurnPrecheckRequest } from "./midturn-precheck.js";
|
||||
import { detachPrePersistedCurrentUserTurn } from "./pre-persisted-user-turn.js";
|
||||
import { PREEMPTIVE_OVERFLOW_ERROR_TEXT } from "./preemptive-compaction.js";
|
||||
import { clearToolActivityRun } from "./tool-activity-heartbeat.js";
|
||||
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
||||
@@ -587,113 +581,25 @@ export async function runEmbeddedAttempt(
|
||||
sessionLockController,
|
||||
sessionManager,
|
||||
});
|
||||
if (isRawModelRun) {
|
||||
// Raw model probes should measure exactly the requested prompt against
|
||||
// the selected provider/model. Reset clears restored transcript state
|
||||
// and queues; the empty system prompt prevents the runtime from rebuilding the
|
||||
// normal OpenClaw agent/tool prompt when `session.prompt()` starts.
|
||||
activeSession.agent.reset();
|
||||
setActiveSessionSystemPrompt("");
|
||||
}
|
||||
const orphanRepair = isRawModelRun
|
||||
? undefined
|
||||
: resolveOrphanRepairPlan({
|
||||
sessionManager,
|
||||
prompt: params.prompt,
|
||||
trigger: params.trigger,
|
||||
});
|
||||
if (orphanRepair?.removeLeaf) {
|
||||
if (orphanRepair.messageEntry.parentId) {
|
||||
sessionManager.branch(orphanRepair.messageEntry.parentId);
|
||||
} else {
|
||||
sessionManager.resetLeaf();
|
||||
}
|
||||
replayTrailingEntriesForOrphanRepair(sessionManager, orphanRepair.trailingEntries);
|
||||
// Suppression assumes the canonical user turn still exists. Orphan repair
|
||||
// removed it, so the replacement prompt must become the one durable copy.
|
||||
sessionManager.clearNextUserMessagePersistenceSuppression?.();
|
||||
params.onUserMessagePersistenceInvalidated?.();
|
||||
activeSession.agent.state.messages = sessionManager.buildSessionContext().messages;
|
||||
}
|
||||
detachPrePersistedCurrentUserTurn({
|
||||
const sessionBoundary = prepareEmbeddedAttemptSessionBoundary({
|
||||
activeSession,
|
||||
attempt: params,
|
||||
isRawModelRun,
|
||||
preparedUserTurnMessage,
|
||||
suppressNextUserMessagePersistence: params.suppressNextUserMessagePersistence,
|
||||
userTurnAlreadyPersisted: params.userTurnTranscriptRecorder?.hasPersisted() === true,
|
||||
sessionManager,
|
||||
setActiveSessionSystemPrompt,
|
||||
});
|
||||
// Single source for the per-message timestamp prefix (issue #3658):
|
||||
// normal embedded runs stamp every user message from its own timestamp.
|
||||
// Raw model probes must keep the requested prompt text exact.
|
||||
const boundaryTimezone = isRawModelRun
|
||||
? undefined
|
||||
: resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||
const includeBoundaryTimestamp =
|
||||
!isRawModelRun && params.config?.agents?.defaults?.envelopeTimestamp !== "off";
|
||||
let currentUserTimestampOverride:
|
||||
| { timestamp: number; text: string; alternateText?: string }
|
||||
| undefined;
|
||||
const buildBoundaryOptions = () => {
|
||||
if (isRawModelRun) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(boundaryTimezone ? { timezone: boundaryTimezone } : {}),
|
||||
...(includeBoundaryTimestamp ? {} : { includeTimestamp: false }),
|
||||
...(currentUserTimestampOverride ? { currentUserTimestampOverride } : {}),
|
||||
};
|
||||
};
|
||||
if (typeof activeSession.agent.convertToLlm === "function") {
|
||||
const baseConvertToLlm = activeSession.agent.convertToLlm.bind(activeSession.agent);
|
||||
activeSession.agent.convertToLlm = async (messages) =>
|
||||
await baseConvertToLlm(
|
||||
// Wire-only: move the current-turn runtime-context carrier to the
|
||||
// absolute tail so the request is an append-only prefix-extension
|
||||
// through the active user turn (see the function's cache rationale).
|
||||
// Applied here, not inside normalizeMessagesForLlmBoundary, because
|
||||
// normalizeMessagesForCurrentPromptBoundary slices off its appended
|
||||
// prompt by position and must not see the carrier relocated past it.
|
||||
relocateCurrentRuntimeContextCarrierToTail(
|
||||
normalizeMessagesForLlmBoundary(messages, buildBoundaryOptions()),
|
||||
),
|
||||
);
|
||||
}
|
||||
const { boundaryTimezone, includeBoundaryTimestamp, orphanRepair } = sessionBoundary;
|
||||
let prePromptMessageCount = activeSession.messages.length;
|
||||
// Session-owned projections survive attempt teardown so already-sent tool results
|
||||
// cannot rewrite the provider prompt-cache tail between turns (#99495).
|
||||
const sessionPromptState = getEmbeddedSessionPromptState(params.sessionId);
|
||||
const toolResultPromptProjectionState = sessionPromptState.toolResults;
|
||||
let contextEngineAfterTurnCheckpoint: number | null = null;
|
||||
const inFlightPromptSettlePromises = new Set<Promise<void>>();
|
||||
const inFlightAbortSettlePromises = new Set<Promise<void>>();
|
||||
const trackSettlePromise = (
|
||||
promises: Set<Promise<void>>,
|
||||
promise: Promise<void>,
|
||||
): Promise<void> => {
|
||||
promises.add(promise);
|
||||
void promise.then(
|
||||
() => {
|
||||
promises.delete(promise);
|
||||
},
|
||||
() => {
|
||||
promises.delete(promise);
|
||||
},
|
||||
);
|
||||
return promise;
|
||||
};
|
||||
const trackPromptSettlePromise = (promise: Promise<void>): Promise<void> =>
|
||||
trackSettlePromise(inFlightPromptSettlePromises, promise);
|
||||
const trackAbortSettlePromise = (promise: Promise<void>): Promise<void> =>
|
||||
trackSettlePromise(inFlightAbortSettlePromises, promise);
|
||||
const abortActiveSession = (reason?: unknown): Promise<void> =>
|
||||
trackAbortSettlePromise(Promise.resolve(activeSession.abort(reason)));
|
||||
const sessionSettleTracker = createEmbeddedAttemptSessionSettleTracker(activeSession);
|
||||
const { abortActiveSession, trackPromptSettlePromise } = sessionSettleTracker;
|
||||
abortActiveSessionForExternalSignal = abortActiveSession;
|
||||
buildAbortSettlePromise = (): Promise<void> | null => {
|
||||
const promises = [...inFlightPromptSettlePromises, ...inFlightAbortSettlePromises];
|
||||
if (promises.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return Promise.allSettled(promises).then(() => undefined);
|
||||
};
|
||||
buildAbortSettlePromise = sessionSettleTracker.buildAbortSettlePromise;
|
||||
abortSessionForYield = () => {
|
||||
yieldAbortSettled = abortActiveSession(SESSIONS_YIELD_ABORT_REASON);
|
||||
};
|
||||
@@ -1255,7 +1161,9 @@ export async function runEmbeddedAttempt(
|
||||
systemPromptForHook,
|
||||
} = promptContext;
|
||||
prePromptMessageCount = promptContext.prePromptMessageCount;
|
||||
currentUserTimestampOverride = promptContext.currentUserTimestampOverride;
|
||||
sessionBoundary.setCurrentUserTimestampOverride(
|
||||
promptContext.currentUserTimestampOverride,
|
||||
);
|
||||
if (aggregatePressureEngaged) {
|
||||
// Compaction and aggregate truncation both target about half the window;
|
||||
// compact-then-truncate prevents re-hitting the same cap on the next turn.
|
||||
|
||||
Reference in New Issue
Block a user