style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,54 +1,66 @@
import type { AutomationStateResponse } from "../types/api";
import type { AutomationStateResponse } from '../types/api'
const DISABLED_AUTOMATION_STATE: AutomationStateResponse = Object.freeze({
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
});
})
function cloneAutomationState(state: AutomationStateResponse): AutomationStateResponse {
return { ...state };
function cloneAutomationState(
state: AutomationStateResponse,
): AutomationStateResponse {
return { ...state }
}
function normalizeAutomationState(raw: unknown): AutomationStateResponse {
if (!raw || typeof raw !== "object") {
return cloneAutomationState(DISABLED_AUTOMATION_STATE);
if (!raw || typeof raw !== 'object') {
return cloneAutomationState(DISABLED_AUTOMATION_STATE)
}
const state = raw as Record<string, unknown>;
const state = raw as Record<string, unknown>
return {
enabled: state.enabled === true,
phase: state.phase === "standby" || state.phase === "sleeping" ? state.phase : null,
next_tick_at: typeof state.next_tick_at === "number" ? state.next_tick_at : null,
sleep_until: typeof state.sleep_until === "number" ? state.sleep_until : null,
};
phase:
state.phase === 'standby' || state.phase === 'sleeping'
? state.phase
: null,
next_tick_at:
typeof state.next_tick_at === 'number' ? state.next_tick_at : null,
sleep_until:
typeof state.sleep_until === 'number' ? state.sleep_until : null,
}
}
function readAutomationStateValue(metadata: Record<string, unknown> | null | undefined): unknown {
if (!metadata || typeof metadata !== "object") {
return undefined;
function readAutomationStateValue(
metadata: Record<string, unknown> | null | undefined,
): unknown {
if (!metadata || typeof metadata !== 'object') {
return undefined
}
if (!Object.prototype.hasOwnProperty.call(metadata, "automation_state")) {
return undefined;
if (!Object.hasOwn(metadata, 'automation_state')) {
return undefined
}
return metadata.automation_state;
return metadata.automation_state
}
export function getAutomationStateSnapshot(
metadata: Record<string, unknown> | null | undefined,
): AutomationStateResponse | undefined {
const raw = readAutomationStateValue(metadata);
const raw = readAutomationStateValue(metadata)
if (raw === undefined) {
return undefined;
return undefined
}
return normalizeAutomationState(raw);
return normalizeAutomationState(raw)
}
export function getAutomationStateEventPayload(
metadata: Record<string, unknown> | null | undefined,
): AutomationStateResponse {
return getAutomationStateSnapshot(metadata) ?? cloneAutomationState(DISABLED_AUTOMATION_STATE);
return (
getAutomationStateSnapshot(metadata) ??
cloneAutomationState(DISABLED_AUTOMATION_STATE)
)
}
export function automationStatesEqual(
@@ -60,5 +72,5 @@ export function automationStatesEqual(
a.phase === b.phase &&
a.next_tick_at === b.next_tick_at &&
a.sleep_until === b.sleep_until
);
)
}

View File

@@ -1,37 +1,47 @@
import { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment, storeMarkAcpAgentOffline } from "../store";
import { storeListSessions } from "../store";
import { config } from "../config";
import { updateSessionStatus } from "./session";
import { log, error as logError } from '../logger'
import {
storeListActiveEnvironments,
storeUpdateEnvironment,
storeMarkAcpAgentOffline,
} from '../store'
import { storeListSessions } from '../store'
import { config } from '../config'
import { updateSessionStatus } from './session'
export function runDisconnectMonitorSweep(now = Date.now()) {
const timeoutMs = config.disconnectTimeout * 1000;
const timeoutMs = config.disconnectTimeout * 1000
// Check environment heartbeat timeout
const envs = storeListActiveEnvironments();
const envs = storeListActiveEnvironments()
for (const env of envs) {
// Skip ACP agents — they use WS keepalive, not polling
if (env.workerType === "acp") {
if (env.workerType === 'acp') {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
log(`[RCS] ACP agent ${env.id} timed out (no activity for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeMarkAcpAgentOffline(env.id);
log(
`[RCS] ACP agent ${env.id} timed out (no activity for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`,
)
storeMarkAcpAgentOffline(env.id)
}
continue;
continue
}
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
log(
`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`,
)
storeUpdateEnvironment(env.id, { status: 'disconnected' })
}
}
// Check session timeout (2x disconnect timeout with no update)
const sessions = storeListSessions();
const sessions = storeListSessions()
for (const session of sessions) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (session.status === 'running' || session.status === 'idle') {
const elapsed = now - session.updatedAt.getTime()
if (elapsed > timeoutMs * 2) {
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
updateSessionStatus(session.id, "inactive");
log(
`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`,
)
updateSessionStatus(session.id, 'inactive')
}
}
}
@@ -39,6 +49,6 @@ export function runDisconnectMonitorSweep(now = Date.now()) {
export function startDisconnectMonitor() {
setInterval(() => {
runDisconnectMonitorSweep();
}, 60_000); // Check every minute
runDisconnectMonitorSweep()
}, 60_000) // Check every minute
}

View File

@@ -1,4 +1,4 @@
import { config } from "../config";
import { config } from '../config'
import {
storeCreateEnvironment,
storeCreateSession,
@@ -7,9 +7,12 @@ import {
storeListActiveEnvironments,
storeListActiveEnvironmentsByUsername,
storeListSessionsByEnvironment,
} from "../store";
import type { RegisterEnvironmentRequest, EnvironmentResponse } from "../types/api";
import type { EnvironmentRecord } from "../store";
} from '../store'
import type {
RegisterEnvironmentRequest,
EnvironmentResponse,
} from '../types/api'
import type { EnvironmentRecord } from '../store'
function toResponse(row: EnvironmentRecord): EnvironmentResponse {
return {
@@ -23,12 +26,17 @@ function toResponse(row: EnvironmentRecord): EnvironmentResponse {
worker_type: row.workerType,
channel_group_id: row.bridgeId,
capabilities: row.capabilities,
};
}
}
export function registerEnvironment(req: RegisterEnvironmentRequest & { metadata?: { worker_type?: string }; username?: string }) {
const secret = config.apiKeys[0] || "";
const workerType = req.worker_type || req.metadata?.worker_type;
export function registerEnvironment(
req: RegisterEnvironmentRequest & {
metadata?: { worker_type?: string }
username?: string
},
) {
const secret = config.apiKeys[0] || ''
const workerType = req.worker_type || req.metadata?.worker_type
const record = storeCreateEnvironment({
secret,
machineName: req.machine_name,
@@ -40,51 +48,58 @@ export function registerEnvironment(req: RegisterEnvironmentRequest & { metadata
bridgeId: req.bridge_id,
username: req.username,
capabilities: req.capabilities,
});
})
let sessionId: string | undefined;
let sessionId: string | undefined
// ACP agents: reuse existing session or create one
if (workerType === "acp") {
const existing = storeListSessionsByEnvironment(record.id);
if (workerType === 'acp') {
const existing = storeListSessionsByEnvironment(record.id)
if (existing.length > 0) {
sessionId = existing[0].id;
sessionId = existing[0].id
} else {
const session = storeCreateSession({
environmentId: record.id,
title: req.machine_name || "ACP Agent",
source: "acp",
});
sessionId = session.id;
title: req.machine_name || 'ACP Agent',
source: 'acp',
})
sessionId = session.id
}
}
return { environment_id: record.id, environment_secret: record.secret, status: record.status as "active", session_id: sessionId };
return {
environment_id: record.id,
environment_secret: record.secret,
status: record.status as 'active',
session_id: sessionId,
}
}
export function deregisterEnvironment(envId: string) {
storeUpdateEnvironment(envId, { status: "deregistered" });
storeUpdateEnvironment(envId, { status: 'deregistered' })
}
export function getEnvironment(envId: string) {
return storeGetEnvironment(envId);
return storeGetEnvironment(envId)
}
export function updatePollTime(envId: string) {
storeUpdateEnvironment(envId, { lastPollAt: new Date() });
storeUpdateEnvironment(envId, { lastPollAt: new Date() })
}
export function listActiveEnvironments() {
return storeListActiveEnvironments();
return storeListActiveEnvironments()
}
export function listActiveEnvironmentsResponse(): EnvironmentResponse[] {
return storeListActiveEnvironments().map(toResponse);
return storeListActiveEnvironments().map(toResponse)
}
export function listActiveEnvironmentsByUsername(username: string): EnvironmentResponse[] {
return storeListActiveEnvironmentsByUsername(username).map(toResponse);
export function listActiveEnvironmentsByUsername(
username: string,
): EnvironmentResponse[] {
return storeListActiveEnvironmentsByUsername(username).map(toResponse)
}
export function reconnectEnvironment(envId: string) {
storeUpdateEnvironment(envId, { status: "active" });
storeUpdateEnvironment(envId, { status: 'active' })
}

View File

@@ -9,16 +9,32 @@ import {
storeListSessionsByUsername,
storeListSessionsByEnvironment,
storeListSessionsByOwnerUuid,
} from "../store";
import { randomUUID } from "node:crypto";
import { getAllEventBuses, removeEventBus } from "../transport/event-bus";
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
} from '../store'
import { randomUUID } from 'node:crypto'
import { getAllEventBuses, removeEventBus } from '../transport/event-bus'
import type {
CreateSessionRequest,
CreateCodeSessionRequest,
SessionResponse,
SessionSummaryResponse,
} from '../types/api'
const CODE_SESSION_PREFIX = "cse_";
const WEB_SESSION_PREFIX = "session_";
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
const CODE_SESSION_PREFIX = 'cse_'
const WEB_SESSION_PREFIX = 'session_'
const CLOSED_SESSION_STATUSES = new Set(['archived', 'inactive'])
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
function toResponse(row: {
id: string
environmentId: string | null
title: string | null
status: string
source: string
permissionMode: string | null
workerEpoch: number
username: string | null
createdAt: Date
updatedAt: Date
}): SessionResponse {
return {
id: row.id,
environment_id: row.environmentId,
@@ -30,172 +46,200 @@ function toResponse(row: { id: string; environmentId: string | null; title: stri
username: row.username,
created_at: row.createdAt.getTime() / 1000,
updated_at: row.updatedAt.getTime() / 1000,
};
}
}
export function toWebSessionId(sessionId: string): string {
if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId;
return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`;
if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId
return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`
}
function toCompatibleCodeSessionId(sessionId: string): string | null {
if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null;
return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`;
if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null
return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`
}
export function toWebSessionResponse(session: SessionResponse): SessionResponse {
return { ...session, id: toWebSessionId(session.id) };
export function toWebSessionResponse(
session: SessionResponse,
): SessionResponse {
return { ...session, id: toWebSessionId(session.id) }
}
function toWebSessionSummaryResponse(session: SessionSummaryResponse): SessionSummaryResponse {
return { ...session, id: toWebSessionId(session.id) };
function toWebSessionSummaryResponse(
session: SessionSummaryResponse,
): SessionSummaryResponse {
return { ...session, id: toWebSessionId(session.id) }
}
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
export function createSession(
req: CreateSessionRequest & { username?: string },
): SessionResponse {
const record = storeCreateSession({
environmentId: req.environment_id,
title: req.title,
source: req.source,
permissionMode: req.permission_mode,
username: req.username,
});
return toResponse(record);
})
return toResponse(record)
}
export function createCodeSession(req: CreateCodeSessionRequest): SessionResponse {
export function createCodeSession(
req: CreateCodeSessionRequest,
): SessionResponse {
const record = storeCreateSession({
idPrefix: "cse_",
idPrefix: 'cse_',
title: req.title,
source: req.source,
permissionMode: req.permission_mode,
});
return toResponse(record);
})
return toResponse(record)
}
export function getSession(sessionId: string): SessionResponse | null {
const record = storeGetSession(sessionId);
return record ? toResponse(record) : null;
const record = storeGetSession(sessionId)
return record ? toResponse(record) : null
}
export function isSessionClosedStatus(status: string | null | undefined): boolean {
return !!status && CLOSED_SESSION_STATUSES.has(status);
export function isSessionClosedStatus(
status: string | null | undefined,
): boolean {
return !!status && CLOSED_SESSION_STATUSES.has(status)
}
export function resolveExistingSessionId(sessionId: string): string | null {
if (storeGetSession(sessionId)) {
return sessionId;
return sessionId
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId)
if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) {
return compatibleCodeSessionId;
return compatibleCodeSessionId
}
return null;
return null
}
export function resolveExistingWebSessionId(sessionId: string): string | null {
return resolveExistingSessionId(sessionId);
return resolveExistingSessionId(sessionId)
}
export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null {
export function resolveOwnedWebSessionId(
sessionId: string,
uuid: string,
): string | null {
if (storeIsSessionOwner(sessionId, uuid)) {
return sessionId;
return sessionId
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) {
return compatibleCodeSessionId;
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId)
if (
compatibleCodeSessionId &&
storeIsSessionOwner(compatibleCodeSessionId, uuid)
) {
return compatibleCodeSessionId
}
// Auto-bind: if the session exists but has no owner, claim it for the requesting user
const existingId = resolveExistingSessionId(sessionId);
const existingId = resolveExistingSessionId(sessionId)
if (existingId) {
const owners = storeGetSessionOwners(existingId);
const owners = storeGetSessionOwners(existingId)
if (!owners || owners.size === 0) {
storeBindSession(existingId, uuid);
return existingId;
storeBindSession(existingId, uuid)
return existingId
}
}
return null;
return null
}
export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.filter(session => !isSessionClosedStatus(session.status))
.map(toResponse)
.map(toWebSessionResponse);
.map(toWebSessionResponse)
}
export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
export function listWebSessionSummariesByOwnerUuid(
uuid: string,
): SessionSummaryResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.filter(session => !isSessionClosedStatus(session.status))
.map(toSummaryResponse)
.map(toWebSessionSummaryResponse);
.map(toWebSessionSummaryResponse)
}
export function updateSessionTitle(sessionId: string, title: string) {
storeUpdateSession(sessionId, { title });
storeUpdateSession(sessionId, { title })
}
export function updateSessionStatus(sessionId: string, status: string) {
storeUpdateSession(sessionId, { status });
const bus = getAllEventBuses().get(sessionId);
if (!bus) return;
storeUpdateSession(sessionId, { status })
const bus = getAllEventBuses().get(sessionId)
if (!bus) return
bus.publish({
id: randomUUID(),
sessionId,
type: "session_status",
type: 'session_status',
payload: { status },
direction: "inbound",
});
direction: 'inbound',
})
}
export function touchSession(sessionId: string) {
storeUpdateSession(sessionId, {});
storeUpdateSession(sessionId, {})
}
export function archiveSession(sessionId: string) {
updateSessionStatus(sessionId, "archived");
removeEventBus(sessionId);
updateSessionStatus(sessionId, 'archived')
removeEventBus(sessionId)
}
export function incrementEpoch(sessionId: string): number {
const record = storeGetSession(sessionId);
if (!record) throw new Error("Session not found");
const newEpoch = record.workerEpoch + 1;
storeUpdateSession(sessionId, { workerEpoch: newEpoch });
return newEpoch;
const record = storeGetSession(sessionId)
if (!record) throw new Error('Session not found')
const newEpoch = record.workerEpoch + 1
storeUpdateSession(sessionId, { workerEpoch: newEpoch })
return newEpoch
}
export function listSessions() {
return storeListSessions().map(toResponse);
return storeListSessions().map(toResponse)
}
function toSummaryResponse(row: { id: string; title: string | null; status: string; username: string | null; updatedAt: Date }): SessionSummaryResponse {
function toSummaryResponse(row: {
id: string
title: string | null
status: string
username: string | null
updatedAt: Date
}): SessionSummaryResponse {
return {
id: row.id,
title: row.title,
status: row.status,
username: row.username,
updated_at: row.updatedAt.getTime() / 1000,
};
}
}
export function listSessionSummaries(): SessionSummaryResponse[] {
return storeListSessions().map(toSummaryResponse);
return storeListSessions().map(toSummaryResponse)
}
export function listSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
return storeListSessionsByOwnerUuid(uuid).map(toSummaryResponse);
export function listSessionSummariesByOwnerUuid(
uuid: string,
): SessionSummaryResponse[] {
return storeListSessionsByOwnerUuid(uuid).map(toSummaryResponse)
}
export function listSessionSummariesByUsername(username: string): SessionSummaryResponse[] {
return storeListSessionsByUsername(username).map(toSummaryResponse);
export function listSessionSummariesByUsername(
username: string,
): SessionSummaryResponse[] {
return storeListSessionsByUsername(username).map(toSummaryResponse)
}
export function listSessionsByEnvironment(envId: string) {
return storeListSessionsByEnvironment(envId).map(toResponse);
return storeListSessionsByEnvironment(envId).map(toResponse)
}

View File

@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { getEventBus } from "../transport/event-bus";
import { randomUUID } from 'node:crypto'
import { getEventBus } from '../transport/event-bus'
/**
* Extract plain text from various message payload formats.
@@ -9,75 +9,87 @@ import { getEventBus } from "../transport/event-bus";
* { message: { content: [{type:"text",text:"..."}] } }
*/
function extractContent(payload: unknown): string {
if (!payload || typeof payload !== "object") {
return typeof payload === "string" ? payload : "";
if (!payload || typeof payload !== 'object') {
return typeof payload === 'string' ? payload : ''
}
const p = payload as Record<string, unknown>;
const p = payload as Record<string, unknown>
// Direct content field
if (typeof p.content === "string" && p.content) return p.content;
if (typeof p.content === 'string' && p.content) return p.content
// message.content (child process format)
const msg = p.message;
if (msg && typeof msg === "object") {
const mc = (msg as Record<string, unknown>).content;
if (typeof mc === "string") return mc;
const msg = p.message
if (msg && typeof msg === 'object') {
const mc = (msg as Record<string, unknown>).content
if (typeof mc === 'string') return mc
if (Array.isArray(mc)) {
return mc
.filter((b: unknown) => typeof b === "object" && b !== null && (b as Record<string, unknown>).type === "text")
.map((b: Record<string, unknown>) => (b as Record<string, unknown>).text || "")
.join("");
.filter(
(b: unknown) =>
typeof b === 'object' &&
b !== null &&
(b as Record<string, unknown>).type === 'text',
)
.map(
(b: Record<string, unknown>) =>
(b as Record<string, unknown>).text || '',
)
.join('')
}
}
return "";
return ''
}
/**
* Normalize event payload into a flat structure with guaranteed `content` string.
* Preserves original payload in `raw` field and keeps tool-specific fields.
*/
export function normalizePayload(type: string, payload: unknown): Record<string, unknown> {
if (!payload || typeof payload !== "object") {
return { content: typeof payload === "string" ? payload : "", raw: payload };
export function normalizePayload(
type: string,
payload: unknown,
): Record<string, unknown> {
if (!payload || typeof payload !== 'object') {
return { content: typeof payload === 'string' ? payload : '', raw: payload }
}
const p = payload as Record<string, unknown>;
const content = extractContent(payload);
const p = payload as Record<string, unknown>
const content = extractContent(payload)
const normalized: Record<string, unknown> = {
content,
raw: payload,
};
if (typeof p.uuid === "string" && p.uuid) normalized.uuid = p.uuid;
if (typeof p.isSynthetic === "boolean") normalized.isSynthetic = p.isSynthetic;
if (typeof p.status === "string") normalized.status = p.status;
if (typeof p.subtype === "string") normalized.subtype = p.subtype;
// Preserve tool fields
if (p.tool_name) normalized.tool_name = p.tool_name;
if (p.name) normalized.tool_name = p.name;
if (p.tool_input) normalized.tool_input = p.tool_input;
if (p.input) normalized.tool_input = p.input;
// Preserve permission fields
if (p.request_id) normalized.request_id = p.request_id;
if (p.request) normalized.request = p.request;
if (p.approved !== undefined) normalized.approved = p.approved;
if (p.updated_input) normalized.updated_input = p.updated_input;
// Preserve message field for backward compat
if (p.message) normalized.message = p.message;
if (type === "task_state") {
if (typeof p.task_list_id === "string") normalized.task_list_id = p.task_list_id;
if (typeof p.taskListId === "string") normalized.taskListId = p.taskListId;
if (Array.isArray(p.tasks)) normalized.tasks = p.tasks;
}
return normalized;
if (typeof p.uuid === 'string' && p.uuid) normalized.uuid = p.uuid
if (typeof p.isSynthetic === 'boolean') normalized.isSynthetic = p.isSynthetic
if (typeof p.status === 'string') normalized.status = p.status
if (typeof p.subtype === 'string') normalized.subtype = p.subtype
// Preserve tool fields
if (p.tool_name) normalized.tool_name = p.tool_name
if (p.name) normalized.tool_name = p.name
if (p.tool_input) normalized.tool_input = p.tool_input
if (p.input) normalized.tool_input = p.input
// Preserve permission fields
if (p.request_id) normalized.request_id = p.request_id
if (p.request) normalized.request = p.request
if (p.approved !== undefined) normalized.approved = p.approved
if (p.updated_input) normalized.updated_input = p.updated_input
// Preserve message field for backward compat
if (p.message) normalized.message = p.message
if (type === 'task_state') {
if (typeof p.task_list_id === 'string')
normalized.task_list_id = p.task_list_id
if (typeof p.taskListId === 'string') normalized.taskListId = p.taskListId
if (Array.isArray(p.tasks)) normalized.tasks = p.tasks
}
return normalized
}
/** Publish an event to a session's bus (in-memory only) */
@@ -85,12 +97,12 @@ export function publishSessionEvent(
sessionId: string,
type: string,
payload: unknown,
direction: "inbound" | "outbound",
direction: 'inbound' | 'outbound',
) {
const bus = getEventBus(sessionId);
const eventId = randomUUID();
const bus = getEventBus(sessionId)
const eventId = randomUUID()
const normalized = normalizePayload(type, payload);
const normalized = normalizePayload(type, payload)
const event = bus.publish({
id: eventId,
@@ -98,7 +110,7 @@ export function publishSessionEvent(
type,
payload: normalized,
direction,
});
})
return event;
return event
}

View File

@@ -1,4 +1,4 @@
import { log, error as logError } from "../logger";
import { log, error as logError } from '../logger'
import {
storeCreateWorkItem,
storeGetWorkItem,
@@ -6,94 +6,111 @@ import {
storeUpdateWorkItem,
storeListSessionsByEnvironment,
storeGetEnvironment,
} from "../store";
import { config } from "../config";
import { getBaseUrl } from "../config";
import type { WorkResponse } from "../types/api";
} 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] || "",
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");
}
return Buffer.from(JSON.stringify(payload)).toString('base64url')
}
export async function createWorkItem(environmentId: string, sessionId: string): Promise<string> {
export async function createWorkItem(
environmentId: string,
sessionId: string,
): Promise<string> {
// Validate environment exists and is active
const env = storeGetEnvironment(environmentId);
const env = storeGetEnvironment(environmentId)
if (!env) {
throw new Error(`Environment ${environmentId} not found`);
throw new Error(`Environment ${environmentId} not found`)
}
if (env.status !== "active") {
throw new Error(`Environment ${environmentId} is not active (status: ${env.status})`);
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;
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;
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);
const item = storeGetPendingWorkItem(environmentId)
if (item) {
storeUpdateWorkItem(item.id, { state: "dispatched" });
storeUpdateWorkItem(item.id, { state: 'dispatched' })
return {
id: item.id,
type: "work",
type: 'work',
environment_id: environmentId,
state: "dispatched",
state: 'dispatched',
data: {
type: "session",
type: 'session',
id: item.sessionId,
},
secret: item.secret,
created_at: item.createdAt.toISOString(),
};
}
}
await new Promise((r) => setTimeout(r, 500));
await new Promise(r => setTimeout(r, 500))
}
return null;
return null
}
export function ackWork(workId: string) {
storeUpdateWorkItem(workId, { state: "acked" });
storeUpdateWorkItem(workId, { state: 'acked' })
}
export function stopWork(workId: string) {
storeUpdateWorkItem(workId, { state: "completed" });
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();
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",
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);
const activeSessions = storeListSessionsByEnvironment(envId).filter(
s => s.status === 'idle',
)
const promises = activeSessions.map(s => createWorkItem(envId, s.id))
return Promise.all(promises)
}