task logs: migrate from filesystem to postgresql; refactor sidecars into submodules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:49:39 +00:00
co-authored by Claude Opus 4.6
parent 5129f7827f
commit d88fe3cac7
22 changed files with 621 additions and 545 deletions
+124
View File
@@ -0,0 +1,124 @@
import { join } from 'node:path';
import { mkdirSync, existsSync } from 'node:fs';
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
const STATE_DIR = join(DATA_PATH, 'sidecar');
const STATE_FILE = join(STATE_DIR, 'claude-state.json');
const LOCK_FILE = join(STATE_DIR, 'claude.lock');
export type PersistedState = {
proxySecret: string;
claudeSessions: Record<string, string>; // sessionKey → Claude Code session_id
};
const DEFAULT_STATE: PersistedState = {
proxySecret: '',
claudeSessions: {},
};
let currentState: PersistedState = { ...DEFAULT_STATE };
let saveTimer: Timer | null = null;
function ensureDir() {
if (!existsSync(STATE_DIR)) {
mkdirSync(STATE_DIR, { recursive: true });
}
}
export function loadState(): PersistedState {
ensureDir();
try {
if (!existsSync(STATE_FILE)) {
currentState = { ...DEFAULT_STATE };
return currentState;
}
const text = require('node:fs').readFileSync(STATE_FILE, 'utf-8');
currentState = { ...DEFAULT_STATE, ...JSON.parse(text) };
return currentState;
} catch {
currentState = { ...DEFAULT_STATE };
return currentState;
}
}
export async function saveState(): Promise<void> {
ensureDir();
await Bun.write(STATE_FILE, JSON.stringify(currentState, null, 2));
}
export function getState(): PersistedState {
return currentState;
}
export function updateState(patch: Partial<PersistedState>): void {
Object.assign(currentState, patch);
scheduleSave();
}
export function setClaudeSession(sessionKey: string, sessionId: string): void {
currentState.claudeSessions[sessionKey] = sessionId;
scheduleSave();
}
export function clearClaudeSession(sessionKey: string): void {
delete currentState.claudeSessions[sessionKey];
scheduleSave();
}
export function getClaudeSession(sessionKey: string): string | undefined {
return currentState.claudeSessions[sessionKey];
}
function scheduleSave() {
if (saveTimer) return;
saveTimer = setTimeout(async () => {
saveTimer = null;
await saveState();
}, 30_000);
}
export async function flushAndSave(): Promise<void> {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
await saveState();
}
// ── Lockfile ──
export function acquireLock(): boolean {
ensureDir();
try {
if (existsSync(LOCK_FILE)) {
const pidStr = require('node:fs').readFileSync(LOCK_FILE, 'utf-8').trim();
const pid = Number(pidStr);
if (pid && isProcessAlive(pid)) {
return false;
}
}
require('node:fs').writeFileSync(LOCK_FILE, String(process.pid));
return true;
} catch {
return false;
}
}
export function releaseLock(): void {
try {
if (existsSync(LOCK_FILE)) {
require('node:fs').unlinkSync(LOCK_FILE);
}
} catch {
// best effort
}
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}