Merge remote-tracking branch 'origin/farm/e42ff742/fork-prompt-cache-affinity'
This commit is contained in:
@@ -172,7 +172,7 @@ Interactive `/fork` creates a new session from the current one and switches the
|
||||
2. Flushes pending writes.
|
||||
3. Calls `SessionManager.fork()`.
|
||||
4. Copies artifacts directory from old session namespace to new namespace (best-effort; non-ENOENT copy failures are logged, not fatal).
|
||||
5. Updates `agent.sessionId`.
|
||||
5. Updates `agent.sessionId` and inherits the previous provider prompt-cache key unless an explicit prompt-cache key is already pinned.
|
||||
6. Emits `session_switch` with `reason: "fork"`.
|
||||
|
||||
`SessionManager.fork()` behavior:
|
||||
@@ -184,6 +184,7 @@ Interactive `/fork` creates a new session from the current one and switches the
|
||||
- new timestamp
|
||||
- `cwd` unchanged
|
||||
- `parentSession` set to previous session id
|
||||
- `providerPromptCacheKey` set to the previous header's inherited key, or the previous session id when none was pinned
|
||||
- Keeps all non-header entries unchanged in the new file.
|
||||
|
||||
### Non-persistent behavior
|
||||
@@ -200,6 +201,9 @@ Startup `--fork` is resolved before normal session creation:
|
||||
2. Path-like values (`/`, `\`, or `.jsonl`) call `SessionManager.forkFrom(path, cwd, sessionDir)`.
|
||||
3. Other values resolve via `resolveResumableSession(...)`: local sessions first, then global search when `sessionDir` is not forced. Matching accepts lowercased session id prefixes, full JSONL filename prefixes, and timestamp-stripped filename id suffixes.
|
||||
4. The forked file is created in the current cwd/session-dir scope and becomes the active session manager for startup.
|
||||
5. Full-context forks automatically seed `providerPromptCacheKey` from the source header's inherited key, falling back to the source session id. Startup drops that automatic inheritance when `--model`, `--thinking`, `--system-prompt`, `--append-system-prompt`, `--tools`, or `--no-tools` changes the provider route or prompt/tool shape.
|
||||
|
||||
Use `--prompt-cache-key <key>` to pin the provider prompt-cache identity explicitly and independently from both the OMP session id and `--provider-session-id`. `--provider-session-id` continues to control provider session/routing headers and sticky credential selection; `--prompt-cache-key` controls the OpenAI Responses `prompt_cache_key` payload where supported.
|
||||
|
||||
## Resume and continue
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed full-context forks cold-missing OpenAI prompt caches by persisting an inherited provider prompt-cache key separately from the new OMP session id, adding `--prompt-cache-key` for explicit cache affinity, and dropping automatic inheritance when startup changes the model, thinking level, system prompt, or tool schema. ([#5035](https://github.com/can1357/oh-my-pi/issues/5035))
|
||||
|
||||
## [16.3.15] - 2026-07-09
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface Args {
|
||||
noSession?: boolean;
|
||||
sessionDir?: string;
|
||||
providerSessionId?: string;
|
||||
providerPromptCacheKey?: string;
|
||||
fork?: string;
|
||||
/** Collab link to join at startup (set by the `join` subcommand; no CLI flag). */
|
||||
join?: string;
|
||||
|
||||
@@ -141,6 +141,9 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
||||
"--provider-session-id": (result, value) => {
|
||||
result.providerSessionId = value;
|
||||
},
|
||||
"--prompt-cache-key": (result, value) => {
|
||||
result.providerPromptCacheKey = value;
|
||||
},
|
||||
"--session-dir": (result, value) => {
|
||||
result.sessionDir = value;
|
||||
},
|
||||
|
||||
@@ -789,7 +789,8 @@ export function applyResolvedSystemPromptInputs(
|
||||
}
|
||||
}
|
||||
|
||||
async function buildSessionOptions(
|
||||
/** Builds startup session options from parsed CLI flags, scoped models, and resolved session lineage. */
|
||||
export async function buildSessionOptions(
|
||||
parsed: Args,
|
||||
scopedModels: ScopedModel[],
|
||||
sessionManager: SessionManager | undefined,
|
||||
@@ -820,6 +821,25 @@ async function buildSessionOptions(
|
||||
if (parsed.providerSessionId) {
|
||||
options.providerSessionId = parsed.providerSessionId;
|
||||
}
|
||||
if (parsed.providerPromptCacheKey) {
|
||||
options.providerPromptCacheKey = parsed.providerPromptCacheKey;
|
||||
options.providerPromptCacheKeySource = "explicit";
|
||||
} else {
|
||||
const header = sessionManager?.getHeader();
|
||||
const scopedModelOverride = scopedModels.length > 0 && !parsed.continue && !parsed.resume;
|
||||
const forkCacheShapeChanged =
|
||||
scopedModelOverride ||
|
||||
parsed.model !== undefined ||
|
||||
parsed.thinking !== undefined ||
|
||||
parsed.systemPrompt !== undefined ||
|
||||
parsed.appendSystemPrompt !== undefined ||
|
||||
parsed.tools !== undefined ||
|
||||
parsed.noTools === true;
|
||||
if (!forkCacheShapeChanged && header?.providerPromptCacheKey) {
|
||||
options.providerPromptCacheKey = header.providerPromptCacheKey;
|
||||
options.providerPromptCacheKeySource = "fork";
|
||||
}
|
||||
}
|
||||
|
||||
// Model from CLI
|
||||
// - supports --provider <name> --model <pattern>
|
||||
|
||||
@@ -426,6 +426,8 @@ export interface CreateAgentSessionOptions {
|
||||
providerSessionId?: string;
|
||||
/** Optional provider-facing prompt cache key, distinct from request lineage. */
|
||||
providerPromptCacheKey?: string;
|
||||
/** Whether `providerPromptCacheKey` is caller-pinned or inherited from a full fork. */
|
||||
providerPromptCacheKeySource?: "explicit" | "fork";
|
||||
/** Absolute wall-clock deadline in Unix epoch milliseconds. */
|
||||
deadline?: number;
|
||||
|
||||
@@ -1221,6 +1223,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
|
||||
);
|
||||
const providerSessionId = options.providerSessionId ?? sessionManager.getSessionId();
|
||||
const forkCacheShapeChanged =
|
||||
options.model !== undefined ||
|
||||
options.modelPattern !== undefined ||
|
||||
options.thinkingLevel !== undefined ||
|
||||
options.systemPrompt !== undefined ||
|
||||
options.customSystemPrompt !== undefined ||
|
||||
options.appendSystemPrompt !== undefined ||
|
||||
options.toolNames !== undefined ||
|
||||
options.customTools !== undefined;
|
||||
const inheritedPromptCacheKey = forkCacheShapeChanged
|
||||
? undefined
|
||||
: sessionManager.getHeader()?.providerPromptCacheKey;
|
||||
const providerPromptCacheKey = options.providerPromptCacheKey ?? inheritedPromptCacheKey;
|
||||
const providerPromptCacheKeySource =
|
||||
options.providerPromptCacheKey !== undefined
|
||||
? (options.providerPromptCacheKeySource ?? "explicit")
|
||||
: providerPromptCacheKey !== undefined
|
||||
? "fork"
|
||||
: undefined;
|
||||
// Startup model *selection* only needs to know whether auth is configured for
|
||||
// a candidate's provider — never the resolved key bytes. Use the synchronous,
|
||||
// side-effect-free probe (`hasConfiguredAuth`): it refreshes no OAuth tokens,
|
||||
@@ -2704,7 +2725,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
onPayload,
|
||||
onResponse,
|
||||
sessionId: providerSessionId,
|
||||
promptCacheKey: options.providerPromptCacheKey,
|
||||
promptCacheKey: providerPromptCacheKey,
|
||||
deadline: options.deadline,
|
||||
transformContext,
|
||||
transformProviderContext,
|
||||
@@ -2888,6 +2909,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
agentId: resolvedAgentId,
|
||||
agentKind,
|
||||
providerSessionId: options.providerSessionId,
|
||||
providerPromptCacheKeySource,
|
||||
parentEvalSessionId: options.parentEvalSessionId,
|
||||
advisorTools,
|
||||
titleSystemPrompt: options.titleSystemPrompt,
|
||||
|
||||
@@ -809,6 +809,8 @@ export interface AgentSessionConfig {
|
||||
* so that credential sticky selection is consistent with the session's streaming calls.
|
||||
*/
|
||||
providerSessionId?: string;
|
||||
/** Marks `agent.promptCacheKey` as fork-inherited so incompatible route changes can clear it. */
|
||||
providerPromptCacheKeySource?: "explicit" | "fork";
|
||||
/**
|
||||
* Full advisor toolset, pre-built in `createAgentSession` against a distinct,
|
||||
* advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
|
||||
@@ -1713,6 +1715,7 @@ export class AgentSession {
|
||||
#agentKind: "main" | "sub" = "main";
|
||||
#providerSessionId: string | undefined;
|
||||
#freshProviderSessionId: string | undefined;
|
||||
#inheritedProviderPromptCacheKey: string | undefined;
|
||||
#isDisposed = false;
|
||||
// Extension system
|
||||
#extensionRunner: ExtensionRunner | undefined = undefined;
|
||||
@@ -2228,6 +2231,8 @@ export class AgentSession {
|
||||
this.#agentId = config.agentId;
|
||||
this.#agentKind = config.agentKind ?? "main";
|
||||
this.#providerSessionId = config.providerSessionId;
|
||||
this.#inheritedProviderPromptCacheKey =
|
||||
config.providerPromptCacheKeySource === "fork" ? this.agent.promptCacheKey : undefined;
|
||||
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
||||
const event: AgentEvent = {
|
||||
type: "message_update",
|
||||
@@ -5594,6 +5599,23 @@ export class AgentSession {
|
||||
return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
||||
}
|
||||
|
||||
#adoptInheritedProviderPromptCacheKey(): void {
|
||||
const key = this.sessionManager.getHeader()?.providerPromptCacheKey;
|
||||
if (!key) return;
|
||||
if (this.#inheritedProviderPromptCacheKey !== undefined || this.agent.promptCacheKey === undefined) {
|
||||
this.agent.promptCacheKey = key;
|
||||
this.#inheritedProviderPromptCacheKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
#clearInheritedProviderPromptCacheKey(): void {
|
||||
const key = this.#inheritedProviderPromptCacheKey;
|
||||
this.#inheritedProviderPromptCacheKey = undefined;
|
||||
if (key !== undefined && this.agent.promptCacheKey === key) {
|
||||
this.agent.promptCacheKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set agent.sessionId from the session manager and install a dynamic
|
||||
* metadata resolver so every Anthropic API request carries
|
||||
@@ -6399,6 +6421,9 @@ export class AgentSession {
|
||||
if (this.#rebuildSystemPrompt) {
|
||||
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
||||
if (signature !== this.#lastAppliedToolSignature) {
|
||||
if (this.#lastAppliedToolSignature !== undefined) {
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
}
|
||||
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
||||
this.#baseSystemPrompt = built.systemPrompt;
|
||||
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
||||
@@ -6485,9 +6510,16 @@ export class AgentSession {
|
||||
if (!this.#rebuildSystemPrompt) return;
|
||||
const activeToolNames = this.getActiveToolNames();
|
||||
this.#setActiveToolNames?.(activeToolNames);
|
||||
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
||||
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
||||
this.#baseSystemPrompt = built.systemPrompt;
|
||||
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
||||
if (
|
||||
previousBaseSystemPrompt.length !== this.#baseSystemPrompt.length ||
|
||||
previousBaseSystemPrompt.some((part, index) => part !== this.#baseSystemPrompt[index])
|
||||
) {
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
}
|
||||
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
||||
this.#promptModelKey = this.#currentPromptModelKey();
|
||||
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
||||
@@ -8728,6 +8760,7 @@ export class AgentSession {
|
||||
this.#clearCheckpointRuntimeState();
|
||||
this.setTodoPhases([]);
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -8828,6 +8861,7 @@ export class AgentSession {
|
||||
|
||||
// Update agent session ID
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#adoptInheritedProviderPromptCacheKey();
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -9149,6 +9183,9 @@ export class AgentSession {
|
||||
this.#autoThinking = true;
|
||||
this.#autoResolvedLevel = undefined;
|
||||
this.#thinkingLevel = provisional;
|
||||
if (!wasAuto) {
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
}
|
||||
this.#applyThinkingLevelToAgent(provisional);
|
||||
if (persist) {
|
||||
this.settings.set("defaultThinkingLevel", AUTO_THINKING);
|
||||
@@ -9172,6 +9209,7 @@ export class AgentSession {
|
||||
this.#applyThinkingLevelToAgent(effectiveLevel);
|
||||
|
||||
if (isChanging) {
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
this.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
|
||||
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
||||
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
||||
@@ -11580,6 +11618,9 @@ export class AgentSession {
|
||||
const currentModel = this.model;
|
||||
if (currentModel) {
|
||||
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
||||
if (!modelsAreEqual(currentModel, model)) {
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
}
|
||||
}
|
||||
this.agent.setModel(model);
|
||||
|
||||
@@ -14745,6 +14786,7 @@ export class AgentSession {
|
||||
const previousSystemPrompt = this.agent.state.systemPrompt;
|
||||
const previousBaseSystemPromptBeforeMemoryPromotion = this.#baseSystemPromptBeforeMemoryPromotion;
|
||||
const previousFreshProviderSessionId = this.#freshProviderSessionId;
|
||||
const previousInheritedProviderPromptCacheKey = this.#inheritedProviderPromptCacheKey;
|
||||
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
||||
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
||||
: undefined;
|
||||
@@ -14766,6 +14808,8 @@ export class AgentSession {
|
||||
await this.sessionManager.setSessionFile(sessionPath);
|
||||
if (switchingToDifferentSession) {
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
this.#adoptInheritedProviderPromptCacheKey();
|
||||
}
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
@@ -14919,6 +14963,7 @@ export class AgentSession {
|
||||
this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
|
||||
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
||||
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
||||
this.#inheritedProviderPromptCacheKey = previousInheritedProviderPromptCacheKey;
|
||||
this.#checkpointState = previousCheckpointState;
|
||||
this.#pendingRewindReport = previousPendingRewindReport;
|
||||
this.#lastCompletedRewind = previousLastCompletedRewind;
|
||||
@@ -14994,6 +15039,7 @@ export class AgentSession {
|
||||
this.#rehydrateCheckpointRewindState();
|
||||
this.#syncTodoPhasesFromBranch();
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#clearInheritedProviderPromptCacheKey();
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
|
||||
@@ -32,10 +32,14 @@ export interface SessionHeader {
|
||||
timestamp: string;
|
||||
cwd: string;
|
||||
parentSession?: string;
|
||||
/** Provider prompt-cache identity inherited by exact-route full forks. */
|
||||
providerPromptCacheKey?: string;
|
||||
}
|
||||
|
||||
export interface NewSessionOptions {
|
||||
parentSession?: string;
|
||||
/** Provider prompt-cache identity to seed on the new session header. */
|
||||
providerPromptCacheKey?: string;
|
||||
/** Skip flushing the current session and delete it instead of saving. */
|
||||
drop?: boolean;
|
||||
}
|
||||
|
||||
@@ -783,6 +783,7 @@ export class SessionManager {
|
||||
timestamp,
|
||||
cwd: this.#cwd,
|
||||
parentSession: options?.parentSession,
|
||||
providerPromptCacheKey: options?.providerPromptCacheKey,
|
||||
};
|
||||
this.#titleUpdatedAt = timestamp;
|
||||
|
||||
@@ -1050,6 +1051,7 @@ export class SessionManager {
|
||||
timestamp,
|
||||
cwd: this.#cwd,
|
||||
parentSession: parentSessionId,
|
||||
providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
|
||||
};
|
||||
this.#sessionName = this.#header.title;
|
||||
this.#titleSource = this.#header.titleSource;
|
||||
@@ -1888,7 +1890,13 @@ export class SessionManager {
|
||||
|
||||
const sourceHeader = sourceEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
||||
const history = sourceEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
||||
manager.#resetToNewSession({ parentSession: sourceHeader?.id }, options?.sessionFile);
|
||||
manager.#resetToNewSession(
|
||||
{
|
||||
parentSession: sourceHeader?.id,
|
||||
providerPromptCacheKey: sourceHeader?.providerPromptCacheKey ?? sourceHeader?.id,
|
||||
},
|
||||
options?.sessionFile,
|
||||
);
|
||||
manager.#header.title = sourceHeader?.title;
|
||||
manager.#header.titleSource = sourceHeader?.titleSource;
|
||||
manager.#sessionName = manager.#header.title;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
import { type Args, parseArgs } from "@oh-my-pi/pi-coding-agent/cli/args";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import type { ScopedModel } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { buildSessionOptions } from "@oh-my-pi/pi-coding-agent/main";
|
||||
import { type CreateAgentSessionOptions, createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk";
|
||||
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { CURRENT_SESSION_VERSION, type SessionHeader } from "@oh-my-pi/pi-coding-agent/session/session-entries";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const OPENAI_TEST_MODEL = getBundledModel("openai", "gpt-4o-mini");
|
||||
|
||||
interface ArgsWithPromptCacheKey extends Args {
|
||||
providerPromptCacheKey?: string;
|
||||
}
|
||||
|
||||
interface SourceSessionFixture {
|
||||
cwd: string;
|
||||
sourceFile: string;
|
||||
sourceHeader: SessionHeader;
|
||||
forkSessionDir: string;
|
||||
}
|
||||
|
||||
async function createSourceSessionFixture(tempDir: TempDir, parentId: string): Promise<SourceSessionFixture> {
|
||||
const cwd = tempDir.join("project");
|
||||
const sourceDir = tempDir.join("source-sessions");
|
||||
const forkSessionDir = tempDir.join("forked-sessions");
|
||||
await fs.mkdir(cwd, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(forkSessionDir, { recursive: true });
|
||||
const sourceFile = path.join(sourceDir, `${parentId}.jsonl`);
|
||||
const sourceHeader: SessionHeader = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: parentId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd,
|
||||
};
|
||||
await Bun.write(sourceFile, `${JSON.stringify(sourceHeader)}\n`);
|
||||
return { cwd, sourceFile, sourceHeader, forkSessionDir };
|
||||
}
|
||||
|
||||
async function createMinimalSession(
|
||||
tempDir: TempDir,
|
||||
options: CreateAgentSessionOptions,
|
||||
): Promise<{ session: AgentSession; authStorage: AuthStorage }> {
|
||||
const authStorage = await AuthStorage.create(tempDir.join("sdk-auth.db"));
|
||||
authStorage.setRuntimeApiKey("openai", "test-key");
|
||||
const shouldSupplyModel = options.sessionManager?.getHeader()?.parentSession === undefined;
|
||||
const result = await createAgentSession({
|
||||
...options,
|
||||
cwd: options.cwd ?? tempDir.path(),
|
||||
agentDir: tempDir.path(),
|
||||
authStorage,
|
||||
modelRegistry: undefined,
|
||||
model: shouldSupplyModel ? (options.model ?? OPENAI_TEST_MODEL) : options.model,
|
||||
settings: Settings.isolated({
|
||||
"async.enabled": false,
|
||||
"marketplace.autoUpdate": "off",
|
||||
}),
|
||||
disableExtensionDiscovery: true,
|
||||
preloadedExtensions: undefined,
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
promptTemplates: [],
|
||||
slashCommands: [],
|
||||
workspaceTree: {
|
||||
rootPath: options.cwd ?? tempDir.path(),
|
||||
rendered: "",
|
||||
truncated: false,
|
||||
totalLines: 0,
|
||||
agentsMdFiles: [],
|
||||
},
|
||||
enableMCP: false,
|
||||
enableLsp: false,
|
||||
...(options.toolNames !== undefined ? { toolNames: options.toolNames } : {}),
|
||||
});
|
||||
return { session: result.session, authStorage };
|
||||
}
|
||||
|
||||
describe("provider prompt-cache key session affinity", () => {
|
||||
it("parses --prompt-cache-key without folding it into provider session id or prompt text", () => {
|
||||
const parsed = parseArgs([
|
||||
"--provider-session-id",
|
||||
"provider-lineage",
|
||||
"--prompt-cache-key",
|
||||
"cache-affinity",
|
||||
"hello",
|
||||
]);
|
||||
const promptCacheArgs: ArgsWithPromptCacheKey = parsed;
|
||||
|
||||
expect(parsed.providerSessionId).toBe("provider-lineage");
|
||||
expect(promptCacheArgs.providerPromptCacheKey).toBe("cache-affinity");
|
||||
expect(parsed.messages).toEqual(["hello"]);
|
||||
expect(parsed.unrecognizedFlags).toEqual([]);
|
||||
});
|
||||
|
||||
it("creates an agent whose prompt-cache key can differ from provider request lineage", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-prompt-cache-sdk-");
|
||||
let session: AgentSession | undefined;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
try {
|
||||
const created = await createMinimalSession(tempDir, {
|
||||
providerSessionId: "provider-lineage",
|
||||
providerPromptCacheKey: "cache-affinity",
|
||||
sessionManager: SessionManager.inMemory(tempDir.path()),
|
||||
});
|
||||
session = created.session;
|
||||
authStorage = created.authStorage;
|
||||
|
||||
expect(session.agent.sessionId).toBe("provider-lineage");
|
||||
expect(session.agent.promptCacheKey).toBe("cache-affinity");
|
||||
expect(session.agent.promptCacheKey).not.toBe(session.agent.sessionId);
|
||||
} finally {
|
||||
await session?.dispose();
|
||||
authStorage?.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("initializes a full fork with child request lineage and parent prompt-cache affinity", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-prompt-cache-fork-");
|
||||
const source = await createSourceSessionFixture(tempDir, "parent-cache-session");
|
||||
const forkedManager = await SessionManager.forkFrom(source.sourceFile, source.cwd, source.forkSessionDir);
|
||||
let session: AgentSession | undefined;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
try {
|
||||
const created = await createMinimalSession(tempDir, {
|
||||
cwd: source.cwd,
|
||||
sessionManager: forkedManager,
|
||||
});
|
||||
session = created.session;
|
||||
authStorage = created.authStorage;
|
||||
const childSessionId = forkedManager.getSessionId();
|
||||
|
||||
expect(forkedManager.getHeader()?.parentSession).toBe(source.sourceHeader.id);
|
||||
expect(childSessionId).toBeString();
|
||||
expect(childSessionId).not.toBe(source.sourceHeader.id);
|
||||
expect(session.agent.sessionId).toBe(childSessionId);
|
||||
expect(session.agent.promptCacheKey).toBe(source.sourceHeader.id);
|
||||
expect(session.agent.promptCacheKey).not.toBe(session.agent.sessionId);
|
||||
} finally {
|
||||
await session?.dispose();
|
||||
authStorage?.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not auto-inherit parent prompt-cache affinity when fork startup changes request-shaping inputs", async () => {
|
||||
const cases: Array<{ name: string; options: CreateAgentSessionOptions }> = [
|
||||
{
|
||||
name: "model",
|
||||
options: { model: OPENAI_TEST_MODEL },
|
||||
},
|
||||
{
|
||||
name: "thinking",
|
||||
options: { thinkingLevel: ThinkingLevel.High },
|
||||
},
|
||||
{
|
||||
name: "system",
|
||||
options: { customSystemPrompt: "Use a different provider prompt." },
|
||||
},
|
||||
{
|
||||
name: "tools",
|
||||
options: { toolNames: ["read"] },
|
||||
},
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
using tempDir = TempDir.createSync(`@omp-prompt-cache-fork-${entry.name}-`);
|
||||
const source = await createSourceSessionFixture(tempDir, `parent-cache-session-${entry.name}`);
|
||||
const forkedManager = await SessionManager.forkFrom(source.sourceFile, source.cwd, source.forkSessionDir);
|
||||
let session: AgentSession | undefined;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
try {
|
||||
const created = await createMinimalSession(tempDir, {
|
||||
...entry.options,
|
||||
cwd: source.cwd,
|
||||
sessionManager: forkedManager,
|
||||
});
|
||||
session = created.session;
|
||||
authStorage = created.authStorage;
|
||||
|
||||
expect(forkedManager.getHeader()?.parentSession).toBe(source.sourceHeader.id);
|
||||
expect(session.agent.promptCacheKey, entry.name).toBeUndefined();
|
||||
} finally {
|
||||
await session?.dispose();
|
||||
authStorage?.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("does not pre-pin parent prompt-cache affinity when a scoped model selects the startup route", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-prompt-cache-scoped-model-");
|
||||
const source = await createSourceSessionFixture(tempDir, "parent-cache-session-scoped");
|
||||
const forkedManager = await SessionManager.forkFrom(source.sourceFile, source.cwd, source.forkSessionDir);
|
||||
const authStorage = await AuthStorage.create(tempDir.join("scoped-auth.db"));
|
||||
authStorage.setRuntimeApiKey(OPENAI_TEST_MODEL.provider, "test-key");
|
||||
try {
|
||||
const modelRegistry = new ModelRegistry(authStorage, tempDir.join("models.yml"));
|
||||
const parsed = parseArgs([
|
||||
"--cwd",
|
||||
source.cwd,
|
||||
"--models",
|
||||
`${OPENAI_TEST_MODEL.provider}/${OPENAI_TEST_MODEL.id}`,
|
||||
]);
|
||||
const scopedModels: ScopedModel[] = [
|
||||
{
|
||||
model: OPENAI_TEST_MODEL,
|
||||
explicitThinkingLevel: false,
|
||||
},
|
||||
];
|
||||
|
||||
const options = await buildSessionOptions(
|
||||
parsed,
|
||||
scopedModels,
|
||||
forkedManager,
|
||||
modelRegistry,
|
||||
Settings.isolated({ "marketplace.autoUpdate": "off" }),
|
||||
);
|
||||
|
||||
expect(options.model).toBe(OPENAI_TEST_MODEL);
|
||||
expect(options.providerPromptCacheKey).toBeUndefined();
|
||||
} finally {
|
||||
authStorage.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user