mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
import { log, error as logError } from '../logger'
|
|
import {
|
|
storeCreateWorkItem,
|
|
storeGetWorkItem,
|
|
storeGetPendingWorkItem,
|
|
storeUpdateWorkItem,
|
|
storeListSessionsByEnvironment,
|
|
storeGetEnvironment,
|
|
} from '../store'
|
|
import { config } from '../config'
|
|
import { getBaseUrl } from '../config'
|
|
import type { WorkResponse } from '../types/api'
|
|
|
|
/** Encode work secret as base64 JSON (no JWT — just API key as token) */
|
|
function encodeWorkSecret(): string {
|
|
const payload = {
|
|
version: 1,
|
|
session_ingress_token: config.apiKeys[0] || '',
|
|
api_base_url: getBaseUrl(),
|
|
sources: [] as string[],
|
|
auth: [] as string[],
|
|
use_code_sessions: false,
|
|
}
|
|
return Buffer.from(JSON.stringify(payload)).toString('base64url')
|
|
}
|
|
|
|
export async function createWorkItem(
|
|
environmentId: string,
|
|
sessionId: string,
|
|
): Promise<string> {
|
|
// Validate environment exists and is active
|
|
const env = storeGetEnvironment(environmentId)
|
|
if (!env) {
|
|
throw new Error(`Environment ${environmentId} not found`)
|
|
}
|
|
if (env.status !== 'active') {
|
|
throw new Error(
|
|
`Environment ${environmentId} is not active (status: ${env.status})`,
|
|
)
|
|
}
|
|
|
|
const secret = encodeWorkSecret()
|
|
const record = storeCreateWorkItem({ environmentId, sessionId, secret })
|
|
log(
|
|
`[RCS] Work item created: ${record.id} for env=${environmentId} session=${sessionId}`,
|
|
)
|
|
return record.id
|
|
}
|
|
|
|
/** Long-poll for work — blocks until work is available or timeout.
|
|
* Returns null when no work is available, matching the CLI bridge client protocol. */
|
|
export async function pollWork(
|
|
environmentId: string,
|
|
timeoutSeconds = config.pollTimeout,
|
|
): Promise<WorkResponse | null> {
|
|
const deadline = Date.now() + timeoutSeconds * 1000
|
|
|
|
while (Date.now() < deadline) {
|
|
const item = storeGetPendingWorkItem(environmentId)
|
|
|
|
if (item) {
|
|
storeUpdateWorkItem(item.id, { state: 'dispatched' })
|
|
|
|
return {
|
|
id: item.id,
|
|
type: 'work',
|
|
environment_id: environmentId,
|
|
state: 'dispatched',
|
|
data: {
|
|
type: 'session',
|
|
id: item.sessionId,
|
|
},
|
|
secret: item.secret,
|
|
created_at: item.createdAt.toISOString(),
|
|
}
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 500))
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function ackWork(workId: string) {
|
|
storeUpdateWorkItem(workId, { state: 'acked' })
|
|
}
|
|
|
|
export function stopWork(workId: string) {
|
|
storeUpdateWorkItem(workId, { state: 'completed' })
|
|
}
|
|
|
|
export function heartbeatWork(workId: string): {
|
|
lease_extended: boolean
|
|
state: string
|
|
last_heartbeat: string
|
|
ttl_seconds: number
|
|
} {
|
|
storeUpdateWorkItem(workId, {} as any) // just bump updatedAt
|
|
const item = storeGetWorkItem(workId)
|
|
const now = new Date()
|
|
return {
|
|
lease_extended: true,
|
|
state: item?.state ?? 'acked',
|
|
last_heartbeat: now.toISOString(),
|
|
ttl_seconds: config.heartbeatInterval * 2,
|
|
}
|
|
}
|
|
|
|
/** Reconnect: re-queue sessions associated with an environment */
|
|
export function reconnectWorkForEnvironment(envId: string) {
|
|
const activeSessions = storeListSessionsByEnvironment(envId).filter(
|
|
s => s.status === 'idle',
|
|
)
|
|
const promises = activeSessions.map(s => createWorkItem(envId, s.id))
|
|
return Promise.all(promises)
|
|
}
|