feat: add session slug generator
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
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 DEFAULT_JOB_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
const MIN_JOB_TTL_MS = 60 * 1000; // 1 minute
|
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;
|
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) {
|
export function addSession(session: ProcessSession) {
|
||||||
runningSessions.set(session.id, session);
|
runningSessions.set(session.id, session);
|
||||||
startSweeper();
|
startSweeper();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
type SessionStdin,
|
type SessionStdin,
|
||||||
addSession,
|
addSession,
|
||||||
appendOutput,
|
appendOutput,
|
||||||
|
createSessionSlug,
|
||||||
markBackgrounded,
|
markBackgrounded,
|
||||||
markExited,
|
markExited,
|
||||||
tail,
|
tail,
|
||||||
@@ -189,7 +189,7 @@ export function createExecTool(
|
|||||||
|
|
||||||
const maxOutput = DEFAULT_MAX_OUTPUT;
|
const maxOutput = DEFAULT_MAX_OUTPUT;
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const sessionId = randomUUID();
|
const sessionId = createSessionSlug();
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const backgroundRequested = params.background === true;
|
const backgroundRequested = params.background === true;
|
||||||
const yieldRequested = typeof params.yieldMs === "number";
|
const yieldRequested = typeof params.yieldMs === "number";
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function createProcessTool(
|
|||||||
.sort((a, b) => b.startedAt - a.startedAt)
|
.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);
|
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,
|
s.status,
|
||||||
9,
|
9,
|
||||||
)} ${formatDuration(s.runtimeMs)} :: ${label}`;
|
)} ${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