OpenClaw plugin that manages long-lived interactive Claude Code processes per OpenClaw session. Process manager + session-mapping persistence + bridge WebSocket server skeleton. Will be rewritten to follow the contractor-agent HTTP model-provider pattern (register as `synthesis-claude-bridge` provider, receive /v1/chat/completions, dispatch to channel notification on the bound Claude process). See parent repo's STATUS.md for the punch list.
84 lines
2.1 KiB
TypeScript
84 lines
2.1 KiB
TypeScript
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
import { dirname } from 'node:path'
|
|
import { randomUUID } from 'node:crypto'
|
|
|
|
export interface SessionRecord {
|
|
openclawSessionId: string
|
|
claudeSessionUuid: string
|
|
createdAt: number
|
|
lastActiveAt: number
|
|
}
|
|
|
|
/**
|
|
* Persistent openclaw_session ↔ claude_session mapping.
|
|
*
|
|
* Single-writer (the OpenclawPlugin process). File-level on every mutation —
|
|
* simple and correct for the volume we expect. If it ever matters, swap for
|
|
* sqlite.
|
|
*/
|
|
export class SessionMapping {
|
|
private records = new Map<string, SessionRecord>()
|
|
private path: string
|
|
|
|
constructor(path: string) {
|
|
this.path = path
|
|
this.load()
|
|
}
|
|
|
|
private load(): void {
|
|
try {
|
|
const raw = readFileSync(this.path, 'utf8')
|
|
const parsed = JSON.parse(raw) as { records?: SessionRecord[] }
|
|
for (const r of parsed.records ?? []) {
|
|
this.records.set(r.openclawSessionId, r)
|
|
}
|
|
} catch {
|
|
// File doesn't exist or unreadable — start fresh.
|
|
}
|
|
}
|
|
|
|
private flush(): void {
|
|
mkdirSync(dirname(this.path), { recursive: true })
|
|
writeFileSync(
|
|
this.path,
|
|
JSON.stringify({ records: [...this.records.values()] }, null, 2),
|
|
'utf8',
|
|
)
|
|
}
|
|
|
|
get(openclawSessionId: string): SessionRecord | undefined {
|
|
return this.records.get(openclawSessionId)
|
|
}
|
|
|
|
/** Get-or-create. Returns the (possibly freshly assigned) claude_session_uuid. */
|
|
ensure(openclawSessionId: string): SessionRecord {
|
|
const existing = this.records.get(openclawSessionId)
|
|
if (existing) return existing
|
|
const now = Date.now()
|
|
const rec: SessionRecord = {
|
|
openclawSessionId,
|
|
claudeSessionUuid: randomUUID(),
|
|
createdAt: now,
|
|
lastActiveAt: now,
|
|
}
|
|
this.records.set(openclawSessionId, rec)
|
|
this.flush()
|
|
return rec
|
|
}
|
|
|
|
touch(openclawSessionId: string): void {
|
|
const r = this.records.get(openclawSessionId)
|
|
if (!r) return
|
|
r.lastActiveAt = Date.now()
|
|
this.flush()
|
|
}
|
|
|
|
forget(openclawSessionId: string): void {
|
|
if (this.records.delete(openclawSessionId)) this.flush()
|
|
}
|
|
|
|
all(): SessionRecord[] {
|
|
return [...this.records.values()]
|
|
}
|
|
}
|