feat: add session slug generator

This commit is contained in:
Peter Steinberger
2026-01-17 06:23:21 +00:00
parent bd32cc40e6
commit 5ebfc0738f
5 changed files with 171 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { createSessionSlug as createSessionSlugId } from "./session-slug.js";
const DEFAULT_JOB_TTL_MS = 30 * 60 * 1000; // 30 minutes
const MIN_JOB_TTL_MS = 60 * 1000; // 1 minute
@@ -65,6 +66,14 @@ const finishedSessions = new Map<string, FinishedSession>();
let sweeper: NodeJS.Timeout | null = null;
function isSessionIdTaken(id: string) {
return runningSessions.has(id) || finishedSessions.has(id);
}
export function createSessionSlug(): string {
return createSessionSlugId(isSessionIdTaken);
}
export function addSession(session: ProcessSession) {
runningSessions.set(session.id, session);
startSweeper();

View File

@@ -1,5 +1,4 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
@@ -11,6 +10,7 @@ import {
type SessionStdin,
addSession,
appendOutput,
createSessionSlug,
markBackgrounded,
markExited,
tail,
@@ -189,7 +189,7 @@ export function createExecTool(
const maxOutput = DEFAULT_MAX_OUTPUT;
const startedAt = Date.now();
const sessionId = randomUUID();
const sessionId = createSessionSlug();
const warnings: string[] = [];
const backgroundRequested = params.background === true;
const yieldRequested = typeof params.yieldMs === "number";

View File

@@ -110,7 +110,7 @@ export function createProcessTool(
.sort((a, b) => b.startedAt - a.startedAt)
.map((s) => {
const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);
return `${s.sessionId.slice(0, 8)} ${pad(
return `${s.sessionId} ${pad(
s.status,
9,
)} ${formatDuration(s.runtimeMs)} :: ${label}`;

View File

@@ -0,0 +1,26 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createSessionSlug } from "./session-slug.js";
describe("session slug", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("generates a two-word slug by default", () => {
vi.spyOn(Math, "random").mockReturnValue(0);
const slug = createSessionSlug();
expect(slug).toBe("amber-atlas");
});
it("adds a numeric suffix when the base slug is taken", () => {
vi.spyOn(Math, "random").mockReturnValue(0);
const slug = createSessionSlug((id) => id === "amber-atlas");
expect(slug).toBe("amber-atlas-2");
});
it("falls back to three words when collisions persist", () => {
vi.spyOn(Math, "random").mockReturnValue(0);
const slug = createSessionSlug((id) => id === "amber-atlas" || id === "amber-atlas-2");
expect(slug).toBe("amber-atlas-atlas");
});
});

133
src/agents/session-slug.ts Normal file
View File

@@ -0,0 +1,133 @@
const SLUG_ADJECTIVES = [
"amber",
"briny",
"brisk",
"calm",
"clear",
"cool",
"crisp",
"dawn",
"delta",
"ember",
"faint",
"fast",
"fresh",
"gentle",
"glow",
"good",
"grand",
"keen",
"kind",
"lucky",
"marine",
"mellow",
"mild",
"neat",
"nimble",
"nova",
"oceanic",
"plaid",
"quick",
"quiet",
"rapid",
"salty",
"sharp",
"swift",
"tender",
"tidal",
"tidy",
"tide",
"vivid",
"warm",
"wild",
"young",
];
const SLUG_NOUNS = [
"atlas",
"basil",
"bison",
"bloom",
"breeze",
"canyon",
"cedar",
"claw",
"cloud",
"comet",
"coral",
"cove",
"crest",
"crustacean",
"daisy",
"dune",
"ember",
"falcon",
"fjord",
"forest",
"glade",
"gulf",
"harbor",
"haven",
"kelp",
"lagoon",
"lobster",
"meadow",
"mist",
"nudibranch",
"nexus",
"ocean",
"orbit",
"otter",
"pine",
"prairie",
"reef",
"ridge",
"river",
"rook",
"sable",
"sage",
"seaslug",
"shell",
"shoal",
"shore",
"slug",
"summit",
"tidepool",
"trail",
"valley",
"wharf",
"willow",
"zephyr",
];
function randomChoice(values: string[], fallback: string) {
return values[Math.floor(Math.random() * values.length)] ?? fallback;
}
function createSlugBase(words = 2) {
const parts = [randomChoice(SLUG_ADJECTIVES, "steady"), randomChoice(SLUG_NOUNS, "harbor")];
if (words > 2) parts.push(randomChoice(SLUG_NOUNS, "reef"));
return parts.join("-");
}
export function createSessionSlug(isTaken?: (id: string) => boolean): string {
const isIdTaken = isTaken ?? (() => false);
for (let attempt = 0; attempt < 12; attempt += 1) {
const base = createSlugBase(2);
if (!isIdTaken(base)) return base;
for (let i = 2; i <= 12; i += 1) {
const candidate = `${base}-${i}`;
if (!isIdTaken(candidate)) return candidate;
}
}
for (let attempt = 0; attempt < 12; attempt += 1) {
const base = createSlugBase(3);
if (!isIdTaken(base)) return base;
for (let i = 2; i <= 12; i += 1) {
const candidate = `${base}-${i}`;
if (!isIdTaken(candidate)) return candidate;
}
}
const fallback = `${createSlugBase(3)}-${Math.random().toString(36).slice(2, 5)}`;
return isIdTaken(fallback) ? `${fallback}-${Date.now().toString(36)}` : fallback;
}