fix: align OpenAI cache affinity and use uuidv7 session ids

This commit is contained in:
Mario Zechner
2026-04-14 23:20:13 +02:00
parent d62d22173a
commit 018b40c30c
11 changed files with 245 additions and 13 deletions

View File

@@ -55,6 +55,7 @@ How to disable it:
- Fixed Gemma 4 thinking level mapping to route between `MINIMAL` and `HIGH`, and map Pi reasoning levels to the model's supported thinking levels ([#2903](https://github.com/badlogic/pi-mono/pull/2903) by [@aadishv](https://github.com/aadishv))
- Fixed Gemini 2.5 Flash Lite minimal thinking budget to use the model's supported 512-token minimum instead of the regular Flash 128-token minimum, avoiding invalid thinking budget errors ([#2861](https://github.com/badlogic/pi-mono/pull/2861) by [@JasonOA888](https://github.com/JasonOA888))
- Fixed OpenAI Codex Responses requests to forward configured `serviceTier` values, restoring service-tier selection for Codex sessions ([#2996](https://github.com/badlogic/pi-mono/pull/2996) by [@markusylisiurunen](https://github.com/markusylisiurunen))
- Fixed newly generated session IDs to use UUIDv7, improving time locality for session-based request routing ([#3018](https://github.com/badlogic/pi-mono/pull/3018) by [@steipete](https://github.com/steipete))
- Fixed `Container.render()` stack overflow on long sessions by replacing `Array.push(...spread)` with a loop-based push, preventing `RangeError: Maximum call stack size exceeded` when child output exceeds the V8 call stack argument limit ([#2651](https://github.com/badlogic/pi-mono/issues/2651))
- Fixed editor sticky-column tracking around paste markers so vertical cursor navigation restores the column from before the cursor entered a paste marker instead of jumping inside or past pasted content ([#3092](https://github.com/badlogic/pi-mono/pull/3092) by [@Perlence](https://github.com/Perlence))
- Fixed queued messages typed during `/tree` branch summarization to flush automatically after navigation completes, so they no longer remain stuck in the steering queue ([#3091](https://github.com/badlogic/pi-mono/pull/3091) by [@Perlence](https://github.com/Perlence))

View File

@@ -58,6 +58,7 @@
"proper-lockfile": "^4.1.2",
"strip-ansi": "^7.1.0",
"undici": "^7.19.1",
"uuid": "^11.1.0",
"yaml": "^2.8.2"
},
"overrides": {

View File

@@ -15,6 +15,7 @@ import {
} from "fs";
import { readdir, readFile, stat } from "fs/promises";
import { join, resolve } from "path";
import { v7 as uuidv7 } from "uuid";
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
import {
type BashExecutionMessage,
@@ -197,6 +198,10 @@ export type ReadonlySessionManager = Pick<
| "getSessionName"
>;
function createSessionId(): string {
return uuidv7();
}
/** Generate a unique short ID (8 hex chars, collision-checked) */
function generateId(byId: { has(id: string): boolean }): string {
for (let i = 0; i < 100; i++) {
@@ -707,7 +712,7 @@ export class SessionManager {
}
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
this.sessionId = header?.id ?? randomUUID();
this.sessionId = header?.id ?? createSessionId();
if (migrateToCurrentVersion(this.fileEntries)) {
this._rewriteFile();
@@ -723,7 +728,7 @@ export class SessionManager {
}
newSession(options?: NewSessionOptions): string | undefined {
this.sessionId = options?.id ?? randomUUID();
this.sessionId = options?.id ?? createSessionId();
const timestamp = new Date().toISOString();
const header: SessionHeader = {
type: "session",
@@ -1172,7 +1177,7 @@ export class SessionManager {
// Filter out LabelEntry from path - we'll recreate them from the resolved map
const pathWithoutLabels = path.filter((e) => e.type !== "label");
const newSessionId = randomUUID();
const newSessionId = createSessionId();
const timestamp = new Date().toISOString();
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
@@ -1325,7 +1330,7 @@ export class SessionManager {
}
// Create new session file with new ID but forked content
const newSessionId = randomUUID();
const newSessionId = createSessionId();
const timestamp = new Date().toISOString();
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
const newSessionFile = join(dir, `${fileTimestamp}_${newSessionId}.jsonl`);

View File

@@ -1,6 +1,11 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { SessionManager } from "../../src/core/session-manager.js";
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
describe("SessionManager.newSession with custom id", () => {
it("uses the provided id instead of generating one", () => {
const session = SessionManager.inMemory();
@@ -8,20 +13,22 @@ describe("SessionManager.newSession with custom id", () => {
expect(session.getSessionId()).toBe("my-custom-id");
});
it("generates a random id when no id is provided", () => {
it("generates a UUIDv7 id when no id is provided", () => {
const session = SessionManager.inMemory();
session.newSession();
const id = session.getSessionId();
expect(id).toBeDefined();
expect(id).not.toBe("");
expect(id).toMatch(UUID_V7_RE);
});
it("generates a random id when options is provided without id", () => {
it("generates a UUIDv7 id when options is provided without id", () => {
const session = SessionManager.inMemory();
session.newSession({ parentSession: "parent.jsonl" });
const id = session.getSessionId();
expect(id).toBeDefined();
expect(id).not.toBe("");
expect(id).toMatch(UUID_V7_RE);
});
it("includes the custom id in the session header", () => {
@@ -32,4 +39,71 @@ describe("SessionManager.newSession with custom id", () => {
expect(header).not.toBeNull();
expect(header!.id).toBe("header-test-id");
});
it("generates a UUIDv7 id when constructed without an explicit id", () => {
const session = SessionManager.inMemory();
expect(session.getSessionId()).toMatch(UUID_V7_RE);
expect(session.getHeader()!.id).toBe(session.getSessionId());
});
it("generates a UUIDv7 id when creating a branched session", () => {
const session = SessionManager.inMemory();
const firstId = session.appendMessage({
role: "user",
content: [{ type: "text", text: "hello" }],
timestamp: Date.now(),
});
session.createBranchedSession(firstId);
expect(session.getSessionId()).toMatch(UUID_V7_RE);
expect(session.getHeader()!.id).toBe(session.getSessionId());
});
it("generates a UUIDv7 id when forking from another session file", () => {
const tempDir = mkdtempSync(join(tmpdir(), "pi-session-manager-"));
const sourcePath = join(tempDir, "source.jsonl");
writeFileSync(
sourcePath,
`${[
JSON.stringify({
type: "session",
version: 3,
id: "legacy-session-id",
timestamp: new Date().toISOString(),
cwd: tempDir,
}),
JSON.stringify({
type: "message",
id: "entry-1",
parentId: null,
timestamp: new Date().toISOString(),
message: {
role: "assistant",
content: [{ type: "text", text: "hello" }],
api: "openai-responses",
provider: "openai",
model: "gpt-5.4",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
},
}),
].join("\n")}
`,
);
const forked = SessionManager.forkFrom(sourcePath, tempDir, tempDir);
const header = forked.getHeader();
expect(header).not.toBeNull();
expect(header!.id).toMatch(UUID_V7_RE);
expect(header!.parentSession).toBe(sourcePath);
});
});