fix: lazy session_id — generated on first agent invocation, not at startup

This commit is contained in:
Markov 2026-02-24 11:18:00 +01:00
parent cb618a195e
commit a9b2d43f84
3 changed files with 40 additions and 35 deletions

View File

@ -9,8 +9,12 @@
"work_dir": "/root/projects/team-board", "work_dir": "/root/projects/team-board",
"model": "sonnet", "model": "sonnet",
"provider": "anthropic", "provider": "anthropic",
"capabilities": ["coding", "review"], "capabilities": [
"coding",
"review"
],
"max_concurrent_tasks": 2, "max_concurrent_tasks": 2,
"heartbeat_interval_sec": 30, "heartbeat_interval_sec": 30,
"allowed_paths": [] "allowed_paths": [],
"session_id": "858c3419-e6a6-4487-940d-f28508784a49"
} }

View File

@ -36,6 +36,33 @@ export interface AgentConfig {
allowedPaths: string[]; allowedPaths: string[];
/** Persistent session UUID — survives renames. Stored in agent.json. */ /** Persistent session UUID — survives renames. Stored in agent.json. */
sessionId: string; sessionId: string;
/** Internal: path to config file for lazy session_id write-back */
_configPath: string | null;
/** Internal: parsed config data for write-back */
_configData: Record<string, unknown>;
}
/**
* Ensure agent has a persistent session_id.
* Call lazily on first agent invocation, not at startup.
* Generates UUID and writes it back to agent.json.
*/
export function ensureSessionId(config: AgentConfig): string {
if (config.sessionId) return config.sessionId;
const id = crypto.randomUUID();
config.sessionId = id;
if (config._configPath) {
try {
config._configData.session_id = id;
fs.writeFileSync(config._configPath, JSON.stringify(config._configData, null, 2) + '\n');
} catch {
// Non-fatal
}
}
return id;
} }
const homeDir = os.homedir(); const homeDir = os.homedir();
@ -162,30 +189,8 @@ export function loadAgentConfig(): AgentConfig {
apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || (file.api_key as string) || '', apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || (file.api_key as string) || '',
heartbeatIntervalSec: (file.heartbeat_interval_sec as number) || 30, heartbeatIntervalSec: (file.heartbeat_interval_sec as number) || 30,
allowedPaths: (file.allowed_paths as string[]) || [], allowedPaths: (file.allowed_paths as string[]) || [],
sessionId: ensureSessionId(file, configPath), sessionId: (file.session_id as string) || '',
_configPath: configPath,
_configData: file,
}; };
} }
/**
* Ensure agent.json has a persistent session_id.
* Generates UUID on first run and writes it back to the config file.
*/
function ensureSessionId(file: Record<string, unknown>, configPath: string | null): string {
if (file.session_id && typeof file.session_id === 'string') {
return file.session_id;
}
const id = crypto.randomUUID();
// Persist to agent.json if we have a path
if (configPath) {
try {
file.session_id = id;
fs.writeFileSync(configPath, JSON.stringify(file, null, 2) + '\n');
} catch {
// Non-fatal — session works in memory
}
}
return id;
}

View File

@ -4,7 +4,7 @@ import { runAgent } from './agent.js';
import { TrackerClient } from './tracker/client.js'; import { TrackerClient } from './tracker/client.js';
import { createTrackerTools } from './tools/index.js'; import { createTrackerTools } from './tools/index.js';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent'; import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import type { AgentConfig } from './config.js'; import { ensureSessionId, type AgentConfig } from './config.js';
import type { TrackerEvent, TrackerTask } from './tracker/types.js'; import type { TrackerEvent, TrackerTask } from './tracker/types.js';
export interface TaskTracker { export interface TaskTracker {
@ -16,20 +16,16 @@ export class EventRouter {
private log = logger.child({ component: 'event-router' }); private log = logger.child({ component: 'event-router' });
private activeTasks = 0; private activeTasks = 0;
private trackerTools: ToolDefinition[]; private trackerTools: ToolDefinition[];
/** Single session ID for the entire agent lifetime. */
private readonly sessionId: string;
constructor( constructor(
private config: AgentConfig, private config: AgentConfig,
private client: TrackerClient, private client: TrackerClient,
private taskTracker: TaskTracker, private taskTracker: TaskTracker,
) { ) {
this.sessionId = config.sessionId;
this.trackerTools = createTrackerTools({ this.trackerTools = createTrackerTools({
trackerClient: client, trackerClient: client,
agentSlug: config.slug, agentSlug: config.slug,
}); });
this.log.info({ toolCount: this.trackerTools.length, sessionId: this.sessionId }, 'Tracker tools registered'); this.log.info({ toolCount: this.trackerTools.length }, 'Tracker tools registered');
} }
async handleEvent(event: TrackerEvent): Promise<void> { async handleEvent(event: TrackerEvent): Promise<void> {
@ -90,7 +86,7 @@ export class EventRouter {
let collectedText = ''; let collectedText = '';
for await (const msg of runAgent(prompt, { for await (const msg of runAgent(prompt, {
workDir: this.config.workDir, workDir: this.config.workDir,
sessionId: this.sessionId, sessionId: ensureSessionId(this.config),
model: this.config.model, model: this.config.model,
provider: this.config.provider, provider: this.config.provider,
systemPrompt: this.config.prompt || undefined, systemPrompt: this.config.prompt || undefined,
@ -162,7 +158,7 @@ export class EventRouter {
let collectedText = ''; let collectedText = '';
for await (const msg of runAgent(content, { for await (const msg of runAgent(content, {
workDir: this.config.workDir, workDir: this.config.workDir,
sessionId: this.sessionId, sessionId: ensureSessionId(this.config),
model: this.config.model, model: this.config.model,
provider: this.config.provider, provider: this.config.provider,
systemPrompt: this.config.prompt || undefined, systemPrompt: this.config.prompt || undefined,