feat: add session slug generator
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
26
src/agents/session-slug.test.ts
Normal file
26
src/agents/session-slug.test.ts
Normal 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
133
src/agents/session-slug.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user