chore: initial scaffolding for SynthesisAgent
Bridge between OpenClaw (multi-channel hub) and interactive Claude Code, keeping autonomous agent usage on the subscription quota after the 2026-06-15 Agent SDK credit split. Initial scaffolding only — two submodules with skeletons: - SynthesisAgent.ClaudePlugin: stdio MCP server registered as a --channels source. Declares experimental.claude/channel capability (verified 2026-05-14 against the official Anthropic discord plugin) and emits notifications/claude/channel to push OpenClaw inbound messages into the Claude turn loop. - SynthesisAgent.OpenclawPlugin: process manager + session mapping + bridge WebSocket. Currently shaped around a custom ws protocol; will be rewritten as a model-provider HTTP server (contractor-agent pattern) so OpenClaw routes inbound channel messages through it via /v1/chat/completions. See STATUS.md for the open punch list and docs/wire-protocol.md for the (soon-to-change) inter-plugin frame schema.
This commit is contained in:
83
SynthesisAgent.OpenclawPlugin/core/session-mapping.ts
Normal file
83
SynthesisAgent.OpenclawPlugin/core/session-mapping.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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()]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user