feat(session): Explicit session id naming (#5076)

This commit is contained in:
Armin Ronacher
2026-05-27 21:32:10 +02:00
committed by GitHub
parent 7c02a55632
commit 52dc08c1f7
7 changed files with 307 additions and 27 deletions

View File

@@ -202,6 +202,14 @@ function createSessionId(): string {
return uuidv7();
}
export function assertValidSessionId(id: string): void {
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
throw new Error(
"Session id must be non-empty, contain only alphanumeric characters, '-', '_', and '.', and start and end with an alphanumeric character",
);
}
}
/** 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++) {
@@ -721,7 +729,13 @@ export class SessionManager {
private labelTimestampsById: Map<string, string> = new Map();
private leafId: string | null = null;
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
private constructor(
cwd: string,
sessionDir: string,
sessionFile: string | undefined,
persist: boolean,
newSessionOptions?: NewSessionOptions,
) {
this.cwd = resolvePath(cwd);
this.sessionDir = normalizePath(sessionDir);
this.persist = persist;
@@ -732,7 +746,7 @@ export class SessionManager {
if (sessionFile) {
this.setSessionFile(sessionFile);
} else {
this.newSession();
this.newSession(newSessionOptions);
}
}
@@ -770,6 +784,9 @@ export class SessionManager {
}
newSession(options?: NewSessionOptions): string | undefined {
if (options?.id !== undefined) {
assertValidSessionId(options.id);
}
this.sessionId = options?.id ?? createSessionId();
const timestamp = new Date().toISOString();
const header: SessionHeader = {
@@ -845,14 +862,23 @@ export class SessionManager {
const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
if (!hasAssistant) {
// Mark as not flushed so when assistant arrives, all entries get written
this.flushed = false;
if (this.flushed) {
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
} else {
// Mark as not flushed so when assistant arrives, all entries get written
this.flushed = false;
}
return;
}
if (!this.flushed) {
for (const e of this.fileEntries) {
appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`);
const fd = openSync(this.sessionFile, "wx");
try {
for (const e of this.fileEntries) {
writeFileSync(fd, `${JSON.stringify(e)}\n`);
}
} finally {
closeSync(fd);
}
this.flushed = true;
} else {
@@ -1308,9 +1334,9 @@ export class SessionManager {
* @param cwd Working directory (stored in session header)
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
*/
static create(cwd: string, sessionDir?: string): SessionManager {
static create(cwd: string, sessionDir?: string, options?: NewSessionOptions): SessionManager {
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
return new SessionManager(cwd, dir, undefined, true);
return new SessionManager(cwd, dir, undefined, true, options);
}
/**
@@ -1356,7 +1382,12 @@ export class SessionManager {
* @param targetCwd Target working directory (where the new session will be stored)
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
*/
static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager {
static forkFrom(
sourcePath: string,
targetCwd: string,
sessionDir?: string,
options?: NewSessionOptions,
): SessionManager {
const resolvedSourcePath = resolvePath(sourcePath);
const resolvedTargetCwd = resolvePath(targetCwd);
const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
@@ -1375,7 +1406,10 @@ export class SessionManager {
}
// Create new session file with new ID but forked content
const newSessionId = createSessionId();
if (options?.id !== undefined) {
assertValidSessionId(options.id);
}
const newSessionId = options?.id ?? createSessionId();
const timestamp = new Date().toISOString();
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
const newSessionFile = join(dir, `${fileTimestamp}_${newSessionId}.jsonl`);
@@ -1389,7 +1423,7 @@ export class SessionManager {
cwd: resolvedTargetCwd,
parentSession: resolvedSourcePath,
};
appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`);
writeFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`, { flag: "wx" });
// Copy all non-header entries from source
for (const entry of sourceEntries) {