Initial commit
This commit is contained in:
@@ -0,0 +1,715 @@
|
||||
/**
|
||||
* collabSync — translates backend collaboration data format to frontend store types.
|
||||
*
|
||||
* Backend uses snake_case; frontend uses camelCase. This module bridges the two.
|
||||
*/
|
||||
|
||||
import type { ChatChannel, ChatMessage, ChannelType } from '../types/chat'
|
||||
import type { KanbanBoard, KanbanColumn, KanbanPhase, KanbanTask, TaskPriority, AgentAnimStatus, ProgressEntry, Session, SessionMode, Project, EmployeeAssignment, RoleAggregatedStatus, RoleWorkItemActivitySection, RoleWorkItemRow, RoleWorkItemSummary, WorkItemGate, WorkItemProgressEntry } from '../types/kanban'
|
||||
import { normalizeProgressLog, normalizeWorkItemLog } from './progressLog'
|
||||
import { canonicalizeSessionExecutionIdentity } from './sessionIdentity'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
const UNIX_MS_THRESHOLD = 1_000_000_000_000
|
||||
const WORK_ITEM_EVENT_RE = /^\[Company:([^\]]+)\]\s*(.*)$/
|
||||
const COMPANY_RUNTIME_EVENT_RE = /^\[Company\]\s*(.*)$/
|
||||
|
||||
function normalizeAgentRuntimeStatus(rawStatus: unknown, rawAgentStatus: unknown): AgentAnimStatus | undefined {
|
||||
if (rawAgentStatus === 'idle' || rawAgentStatus === 'reflecting' || rawAgentStatus === 'tool_active') {
|
||||
return rawAgentStatus
|
||||
}
|
||||
return rawStatus === 'running' ? 'reflecting' : undefined
|
||||
}
|
||||
|
||||
function normalizeEpochMs(raw: unknown): number {
|
||||
const value = typeof raw === 'string' ? Number(raw) : raw
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return Date.now()
|
||||
return value < UNIX_MS_THRESHOLD ? value * 1000 : value
|
||||
}
|
||||
|
||||
function mapBackendProgressLog(raw: any): ProgressEntry[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return normalizeProgressLog(raw
|
||||
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
|
||||
.map((entry) => ({
|
||||
timestamp: normalizeEpochMs(entry.timestamp),
|
||||
type: (entry.type ?? 'status_change') as ProgressEntry['type'],
|
||||
summary: typeof entry.summary === 'string' ? entry.summary : '',
|
||||
detail: typeof entry.detail === 'string' ? entry.detail : undefined,
|
||||
turnId: typeof entry.turn_id === 'string'
|
||||
? entry.turn_id
|
||||
: typeof entry.turnId === 'string'
|
||||
? entry.turnId
|
||||
: undefined,
|
||||
itemId: typeof entry.item_id === 'string'
|
||||
? entry.item_id
|
||||
: typeof entry.itemId === 'string'
|
||||
? entry.itemId
|
||||
: undefined,
|
||||
streamId: typeof entry.stream_id === 'string'
|
||||
? entry.stream_id
|
||||
: typeof entry.streamId === 'string'
|
||||
? entry.streamId
|
||||
: undefined,
|
||||
seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
|
||||
executionMode: typeof entry.execution_mode === 'string'
|
||||
? entry.execution_mode
|
||||
: typeof entry.executionMode === 'string'
|
||||
? entry.executionMode
|
||||
: undefined,
|
||||
})))
|
||||
}
|
||||
|
||||
function mapBackendWorkItemLog(raw: any): WorkItemProgressEntry[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return normalizeWorkItemLog(raw
|
||||
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
|
||||
.map((entry) => {
|
||||
const runtimeTaskId = typeof entry.runtime_task_id === 'string'
|
||||
? entry.runtime_task_id
|
||||
: typeof entry.runtimeTaskId === 'string'
|
||||
? entry.runtimeTaskId
|
||||
: typeof entry.execution_turn_id === 'string'
|
||||
? entry.execution_turn_id
|
||||
: typeof entry.executionTurnId === 'string'
|
||||
? entry.executionTurnId
|
||||
: undefined
|
||||
const executionTurnId = typeof entry.execution_turn_id === 'string'
|
||||
? entry.execution_turn_id
|
||||
: typeof entry.executionTurnId === 'string'
|
||||
? entry.executionTurnId
|
||||
: runtimeTaskId
|
||||
return {
|
||||
timestamp: normalizeEpochMs(entry.timestamp),
|
||||
type: (entry.type ?? 'gate_result') as WorkItemProgressEntry['type'],
|
||||
workItemProjectionId: typeof entry.work_item_projection_id === 'string'
|
||||
? entry.work_item_projection_id
|
||||
: typeof entry.workItemProjectionId === 'string'
|
||||
? entry.workItemProjectionId
|
||||
: undefined,
|
||||
workItemTurnType: typeof entry.work_item_turn_type === 'string'
|
||||
? entry.work_item_turn_type
|
||||
: typeof entry.workItemTurnType === 'string'
|
||||
? entry.workItemTurnType
|
||||
: undefined,
|
||||
workItemProjectionTitle: typeof entry.work_item_projection_title === 'string'
|
||||
? entry.work_item_projection_title
|
||||
: typeof entry.workItemProjectionTitle === 'string'
|
||||
? entry.workItemProjectionTitle
|
||||
: undefined,
|
||||
runtimeTaskId,
|
||||
executionTurnId,
|
||||
roleName: typeof entry.role_name === 'string'
|
||||
? entry.role_name
|
||||
: typeof entry.roleName === 'string'
|
||||
? entry.roleName
|
||||
: undefined,
|
||||
detail: typeof entry.detail === 'string' ? entry.detail : undefined,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function workItemProjectionTitle(projectionId: string): string {
|
||||
return projectionId
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
|
||||
function workItemEntryType(action: string): WorkItemProgressEntry['type'] {
|
||||
const actionLower = action.toLowerCase()
|
||||
if (actionLower.includes('starting') || actionLower.includes('started')) return 'work_item_started'
|
||||
if (actionLower.includes('gate passed') || actionLower.includes('approved') || actionLower.includes('completed')) return 'gate_approved'
|
||||
if (actionLower.includes('rejected') || actionLower.includes('reworking')) return 'gate_rejected'
|
||||
if (actionLower.includes('awaiting peer')) return 'awaiting_peer'
|
||||
if (actionLower.includes('awaiting manager review')) return 'awaiting_manager_review'
|
||||
if (
|
||||
actionLower.includes('awaiting user')
|
||||
|| actionLower.includes('awaiting human review')
|
||||
|| actionLower.includes('awaiting review')
|
||||
|| actionLower.includes('awaiting confirmation')
|
||||
|| actionLower.includes('awaiting feedback')
|
||||
) {
|
||||
return 'awaiting_human'
|
||||
}
|
||||
if (actionLower.includes('failed')) return 'work_item_failed'
|
||||
if (actionLower.includes('deadlock')) return 'deadlock'
|
||||
return 'gate_result'
|
||||
}
|
||||
|
||||
function workItemEntryFromMessage(message: ChatMessage): WorkItemProgressEntry | null {
|
||||
const content = message.content.trim()
|
||||
const meta = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const roleName = message.sender !== 'system' && message.sender !== 'assistant'
|
||||
? message.senderName
|
||||
: undefined
|
||||
const executionTurnId = typeof meta.forwarded_from === 'string'
|
||||
? meta.forwarded_from
|
||||
: typeof meta.task_id === 'string'
|
||||
? meta.task_id
|
||||
: typeof meta.taskId === 'string'
|
||||
? meta.taskId
|
||||
: undefined
|
||||
|
||||
const projectionMatch = WORK_ITEM_EVENT_RE.exec(content)
|
||||
if (projectionMatch) {
|
||||
const [, projectionId, actionRaw] = projectionMatch
|
||||
const action = actionRaw.trim()
|
||||
return {
|
||||
timestamp: message.timestamp,
|
||||
type: workItemEntryType(action),
|
||||
workItemProjectionId: projectionId,
|
||||
workItemProjectionTitle: workItemProjectionTitle(projectionId),
|
||||
roleName,
|
||||
detail: action || undefined,
|
||||
runtimeTaskId: executionTurnId,
|
||||
executionTurnId,
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeMatch = COMPANY_RUNTIME_EVENT_RE.exec(content)
|
||||
if (runtimeMatch) {
|
||||
const action = runtimeMatch[1].trim()
|
||||
return {
|
||||
timestamp: message.timestamp,
|
||||
type: workItemEntryType(action),
|
||||
workItemProjectionId: 'company_runtime',
|
||||
workItemProjectionTitle: 'Company Runtime',
|
||||
roleName,
|
||||
detail: action || undefined,
|
||||
runtimeTaskId: executionTurnId,
|
||||
executionTurnId,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function deriveWorkItemLog(messages: ChatMessage[]): WorkItemProgressEntry[] {
|
||||
return messages
|
||||
.map(workItemEntryFromMessage)
|
||||
.filter((entry): entry is WorkItemProgressEntry => entry != null)
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
|
||||
function hydrateCompanyRuntimeSessions(sessions: Session[], messages: ChatMessage[]): Session[] {
|
||||
if (sessions.length === 0 || messages.length === 0) return sessions
|
||||
|
||||
const messagesByChannel = new Map<string, ChatMessage[]>()
|
||||
for (const message of messages) {
|
||||
const bucket = messagesByChannel.get(message.channelId)
|
||||
if (bucket) bucket.push(message)
|
||||
else messagesByChannel.set(message.channelId, [message])
|
||||
}
|
||||
|
||||
return sessions.map((session) => {
|
||||
const derivedWorkItemLog = deriveWorkItemLog(messagesByChannel.get(session.channelId) ?? [])
|
||||
const workItemLog = session.workItemLog && session.workItemLog.length > 0
|
||||
? session.workItemLog
|
||||
: derivedWorkItemLog
|
||||
const latestConcreteWorkItem = [...workItemLog]
|
||||
.reverse()
|
||||
.find((entry) => {
|
||||
const projectionId = entry.workItemProjectionId
|
||||
return projectionId && projectionId !== 'company_runtime'
|
||||
})
|
||||
const workItemProjectionId = session.mode === 'primary'
|
||||
? (latestConcreteWorkItem?.workItemProjectionId ?? session.workItemProjectionId)
|
||||
: (session.workItemProjectionId ?? latestConcreteWorkItem?.workItemProjectionId)
|
||||
const isCompanyRuntime = session.isCompanyRuntime || (session.mode === 'primary' && workItemLog.length > 0)
|
||||
|
||||
if (
|
||||
workItemLog.length === 0
|
||||
&& workItemProjectionId === session.workItemProjectionId
|
||||
&& isCompanyRuntime === session.isCompanyRuntime
|
||||
) {
|
||||
return session
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
isCompanyRuntime,
|
||||
workItemProjectionId,
|
||||
...(workItemLog.length > 0 ? { workItemLog } : {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hydrateCompanyRuntimeTasks(tasks: KanbanTask[], sessions: Session[]): KanbanTask[] {
|
||||
if (tasks.length === 0 || sessions.length === 0) return tasks
|
||||
|
||||
const sessionsByTaskId = new Map(sessions.map((session) => [session.taskId, session]))
|
||||
return tasks.map((task) => {
|
||||
const session = sessionsByTaskId.get(task.id)
|
||||
if (!session) return task
|
||||
|
||||
const workItemProjectionId = session.workItemProjectionId ?? task.workItemProjectionId
|
||||
const companyProfile = session.companyProfile ?? task.companyProfile
|
||||
const workItemRoleId = session.workItemRoleId ?? task.workItemRoleId
|
||||
const workItemRoleName = session.workItemRoleName ?? task.workItemRoleName
|
||||
|
||||
if (
|
||||
workItemProjectionId === task.workItemProjectionId
|
||||
&& companyProfile === task.companyProfile
|
||||
&& workItemRoleId === task.workItemRoleId
|
||||
&& workItemRoleName === task.workItemRoleName
|
||||
) {
|
||||
return task
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
workItemProjectionId,
|
||||
companyProfile,
|
||||
workItemRoleId,
|
||||
workItemRoleName,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function mapBackendChannel(raw: any): ChatChannel {
|
||||
return {
|
||||
id: raw.channel_id ?? raw.id ?? '',
|
||||
type: (raw.channel_type ?? raw.type ?? 'session') as ChannelType,
|
||||
name: raw.name ?? '',
|
||||
officeId: raw.office_id ?? raw.officeId,
|
||||
participants: raw.participants ?? [],
|
||||
pinned: !!raw.pinned,
|
||||
createdAt: typeof raw.created_at === 'number' ? raw.created_at * 1000 : (raw.createdAt ?? Date.now()),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBackendMessage(raw: any): ChatMessage {
|
||||
const rawSenderName = raw.sender_name ?? raw.senderName ?? ''
|
||||
const senderName = String(rawSenderName).trim().toLowerCase() === 'task generalist'
|
||||
? 'OPC'
|
||||
: rawSenderName
|
||||
return {
|
||||
id: raw.message_id ?? raw.id ?? '',
|
||||
channelId: raw.channel_id ?? raw.channelId ?? '',
|
||||
sender: raw.sender ?? '',
|
||||
senderName,
|
||||
content: raw.content ?? '',
|
||||
timestamp: typeof raw.created_at === 'number' ? raw.created_at * 1000 : (raw.timestamp ?? Date.now()),
|
||||
replyToId: raw.reply_to_id ?? raw.replyToId,
|
||||
mentions: raw.mentions ?? [],
|
||||
metadata: raw.metadata,
|
||||
senderDeleted: !!(raw.sender_deleted ?? raw.senderDeleted),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBackendBoard(raw: any): KanbanBoard {
|
||||
return {
|
||||
id: raw.board_id ?? raw.id ?? '',
|
||||
name: raw.name ?? '',
|
||||
description: raw.description,
|
||||
color: raw.color ?? '#3b82f6',
|
||||
officeId: raw.office_id ?? raw.officeId,
|
||||
prefix: raw.prefix ?? 'T',
|
||||
nextTaskNum: raw.next_task_num ?? raw.nextTaskNum ?? 1,
|
||||
createdAt: typeof raw.created_at === 'number' ? raw.created_at * 1000 : (raw.createdAt ?? Date.now()),
|
||||
updatedAt: typeof raw.updated_at === 'number' ? raw.updated_at * 1000 : (raw.updatedAt ?? Date.now()),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBackendColumn(raw: any): KanbanColumn {
|
||||
return {
|
||||
id: raw.column_id ?? raw.id ?? '',
|
||||
boardId: raw.board_id ?? raw.boardId ?? '',
|
||||
name: raw.name ?? '',
|
||||
color: raw.color ?? '#888',
|
||||
sortOrder: raw.sort_order ?? raw.sortOrder ?? 0,
|
||||
isTerminal: !!(raw.is_terminal ?? raw.isTerminal),
|
||||
// Phase 2: work-item column metadata
|
||||
roleLabel: raw.role_label ?? raw.roleLabel,
|
||||
gateType: raw.gate_type ?? raw.gateType,
|
||||
isParallel: raw.is_parallel ?? raw.isParallel,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBackendTask(raw: any): KanbanTask {
|
||||
const agentStatus = normalizeAgentRuntimeStatus(raw.status, raw.agent_status ?? raw.agentStatus)
|
||||
// Backend now emits the work-item phase plus a `kanban_column` projection.
|
||||
// The legacy `status: <column>` alias on the kanban-card payload was removed
|
||||
// when DelegationWorkItem.status was retired, so we accept either shape and
|
||||
// fall back to the explicit `column_id` for the few payloads that still use
|
||||
// the older field name.
|
||||
const columnId = raw.kanban_column ?? raw.column_id ?? raw.columnId ?? ''
|
||||
const cardId = raw.work_item_id ?? raw.workItemId ?? raw.task_id ?? raw.id ?? ''
|
||||
const runtimeTaskId = raw.runtime_task_id ?? raw.runtimeTaskId
|
||||
?? raw.execution_turn_id ?? raw.executionTurnId
|
||||
?? ((raw.work_item_id ?? raw.workItemId) ? undefined : (raw.task_id ?? raw.id))
|
||||
const executionTurnId = raw.execution_turn_id ?? raw.executionTurnId ?? runtimeTaskId
|
||||
const executionMode = raw.execution_mode ?? raw.executionMode
|
||||
const rawProjectionId = raw.work_item_projection_id ?? raw.workItemProjectionId
|
||||
const isTaskModeRuntime = executionMode === 'task_mode' || rawProjectionId === 'task_mode_execution'
|
||||
return {
|
||||
id: cardId,
|
||||
displayId: raw.display_id ?? raw.displayId ?? '',
|
||||
boardId: raw.board_id ?? raw.boardId ?? '',
|
||||
columnId,
|
||||
title: raw.title ?? '',
|
||||
description: raw.description,
|
||||
priority: (raw.priority ?? null) as TaskPriority | null,
|
||||
assigneeIds: raw.assignee_ids ?? raw.assigneeIds ?? [],
|
||||
tags: raw.tags ?? [],
|
||||
sortOrder: raw.sort_order ?? raw.sortOrder ?? 0,
|
||||
sessionId: raw.session_id ?? raw.sessionId,
|
||||
workItemId: raw.work_item_id ?? raw.workItemId,
|
||||
runtimeTaskId,
|
||||
executionTurnId,
|
||||
createdAt: typeof raw.created_at === 'number' ? raw.created_at * 1000 : (raw.createdAt ?? Date.now()),
|
||||
updatedAt: typeof raw.updated_at === 'number' ? raw.updated_at * 1000 : (raw.updatedAt ?? Date.now()),
|
||||
// Phase 2: agent runtime state
|
||||
agentStatus,
|
||||
currentTool: raw.current_tool ?? raw.currentTool,
|
||||
displayTool: raw.display_tool ?? raw.displayTool ?? raw.current_tool ?? raw.currentTool,
|
||||
toolElapsedMs: raw.tool_elapsed_ms ?? raw.toolElapsedMs,
|
||||
lastToolSummary: raw.last_tool_summary ?? raw.lastToolSummary,
|
||||
contextTokens: raw.context_tokens ?? raw.contextTokens,
|
||||
contextWindow: raw.context_window ?? raw.contextWindow,
|
||||
contextRemainingPct: raw.context_remaining_pct ?? raw.contextRemainingPct,
|
||||
inputTokens: raw.input_tokens ?? raw.inputTokens,
|
||||
outputTokens: raw.output_tokens ?? raw.outputTokens,
|
||||
totalTokens: raw.total_tokens ?? raw.totalTokens,
|
||||
turnCostUsd: raw.turn_cost_usd ?? raw.turnCostUsd,
|
||||
sessionCostUsd: raw.session_cost_usd ?? raw.sessionCostUsd,
|
||||
pendingPermissionCount: raw.pending_permission_count ?? raw.pendingPermissionCount,
|
||||
drainMode: raw.drain_mode ?? raw.drainMode,
|
||||
residentStatus: raw.resident_status ?? raw.residentStatus,
|
||||
actionableInboxCount: raw.actionable_inbox_count ?? raw.actionableInboxCount,
|
||||
protocolBacklogCount: raw.protocol_backlog_count ?? raw.protocolBacklogCount,
|
||||
notificationBacklogCount: raw.notification_backlog_count ?? raw.notificationBacklogCount,
|
||||
latestNotification: raw.latest_notification ?? raw.latestNotification,
|
||||
// Phase 2: work-item runtime & dependencies
|
||||
workItemProjectionId: isTaskModeRuntime ? undefined : rawProjectionId,
|
||||
workItemTurnType: isTaskModeRuntime ? undefined : (raw.work_item_turn_type ?? raw.workItemTurnType),
|
||||
companyProfile: isTaskModeRuntime ? undefined : (raw.company_profile ?? raw.companyProfile),
|
||||
orgId: isTaskModeRuntime ? undefined : (raw.org_id ?? raw.organization_id ?? raw.orgId ?? raw.organizationId),
|
||||
workItemRoleId: isTaskModeRuntime ? undefined : (raw.work_item_role_id ?? raw.workItemRoleId),
|
||||
workItemRoleName: isTaskModeRuntime ? undefined : (raw.work_item_role_name ?? raw.workItemRoleName),
|
||||
workItemGate: isTaskModeRuntime ? undefined : _mapWorkItemGate(raw.work_item_gate ?? raw.workItemGate),
|
||||
employeeAssignment: _mapEmployeeAssignment(raw.employee_assignment ?? raw.employeeAssignment),
|
||||
selectedExecutionAgent: raw.selected_execution_agent ?? raw.selectedExecutionAgent,
|
||||
originChannel: raw.origin_channel ?? raw.originChannel,
|
||||
dependencies: raw.dependencies,
|
||||
progressLog: mapBackendProgressLog(raw.progress_log ?? raw.progressLog),
|
||||
handoffContext: raw.handoff_context ?? raw.handoffContext,
|
||||
phase: raw.phase,
|
||||
runtimeSessionId: raw.runtime_session_id ?? raw.runtimeSessionId,
|
||||
resumeCursor: raw.resume_cursor ?? raw.resumeCursor,
|
||||
worktreePath: raw.worktree_path ?? raw.worktreePath,
|
||||
blockedReason: raw.blocked_reason ?? raw.blockedReason,
|
||||
reviewVerdict: raw.review_verdict ?? raw.reviewVerdict,
|
||||
reviewSummary: raw.review_summary ?? raw.reviewSummary,
|
||||
reviewOwnerRoleId: raw.review_owner_role_id ?? raw.reviewOwnerRoleId,
|
||||
reviewOwnerSeatId: raw.review_owner_seat_id ?? raw.reviewOwnerSeatId,
|
||||
managerRoleId: raw.manager_role_id ?? raw.managerRoleId,
|
||||
managerSeatId: raw.manager_seat_id ?? raw.managerSeatId,
|
||||
scopeKey: raw.scope_key ?? raw.scopeKey,
|
||||
completionReport: raw.completion_report ?? raw.completionReport,
|
||||
reworkFeedback: raw.rework_feedback ?? raw.reworkFeedback,
|
||||
planningContext: raw.planning_context ?? raw.planningContext,
|
||||
deliverables: raw.deliverables,
|
||||
acceptanceCriteria: raw.acceptance_criteria ?? raw.acceptanceCriteria,
|
||||
delegationRationale: raw.delegation_rationale ?? raw.delegationRationale,
|
||||
nonOverlapGuard: raw.non_overlap_guard ?? raw.nonOverlapGuard,
|
||||
coordinationNotes: raw.coordination_notes ?? raw.coordinationNotes,
|
||||
originalMessage: raw.original_message ?? raw.originalMessage,
|
||||
residentAssignment: raw.resident_assignment ?? raw.residentAssignment,
|
||||
memberSessionState: raw.member_session_state ?? raw.memberSessionState,
|
||||
ownershipContract: raw.ownership_contract ?? raw.ownershipContract,
|
||||
}
|
||||
}
|
||||
|
||||
function _mapEmployeeAssignment(raw: any): EmployeeAssignment | undefined {
|
||||
if (!raw || typeof raw !== 'object') return undefined
|
||||
return {
|
||||
name: raw.name,
|
||||
employeeId: raw.employee_id ?? raw.employeeId,
|
||||
category: raw.category,
|
||||
experienceScore: raw.experience_score ?? raw.experienceScore,
|
||||
domains: raw.domains,
|
||||
preferredExternalAgent: raw.preferred_external_agent ?? raw.preferredExternalAgent,
|
||||
promptContext: raw.prompt_context ?? raw.promptContext,
|
||||
deltaContext: raw.delta_context ?? raw.deltaContext,
|
||||
skillRefs: raw.skill_refs ?? raw.skillRefs,
|
||||
}
|
||||
}
|
||||
|
||||
function _mapWorkItemGate(raw: any): WorkItemGate | undefined {
|
||||
if (!raw || typeof raw !== 'object') return undefined
|
||||
return {
|
||||
type: raw.type ?? raw.gate_type ?? raw.gateType,
|
||||
reviewerRole: raw.reviewer_role ?? raw.reviewerRole,
|
||||
autoApprove: raw.auto_approve ?? raw.autoApprove,
|
||||
criteria: raw.criteria,
|
||||
}
|
||||
}
|
||||
|
||||
// Backend phase string is the source of truth — the frontend lives off
|
||||
// that vocabulary too (see ``types/kanban.ts:KanbanPhase``). Validate at
|
||||
// the deserialization boundary so a typo upstream surfaces here, not deep
|
||||
// inside the rendering code.
|
||||
const KNOWN_PHASES: ReadonlySet<KanbanPhase> = new Set<KanbanPhase>([
|
||||
'queued', 'ready', 'ready_for_rework', 'waiting_dependencies',
|
||||
'running', 'waiting_for_peer', 'waiting_for_children', 'paused', 'needs_attention',
|
||||
'awaiting_manager_review', 'awaiting_human',
|
||||
'approved', 'failed', 'cancelled',
|
||||
])
|
||||
|
||||
const KNOWN_AGGREGATED_STATUS: ReadonlySet<RoleAggregatedStatus> = new Set<RoleAggregatedStatus>([
|
||||
'active', 'waiting', 'pending', 'done', 'failed',
|
||||
])
|
||||
|
||||
function coercePhase(value: unknown): KanbanPhase {
|
||||
if (typeof value === 'string' && KNOWN_PHASES.has(value as KanbanPhase)) {
|
||||
return value as KanbanPhase
|
||||
}
|
||||
// Falling back to ``queued`` matches the column id ``todo``, which is
|
||||
// the safest "haven't done anything yet" placeholder.
|
||||
return 'queued'
|
||||
}
|
||||
|
||||
function coerceAggregatedStatus(value: unknown): RoleAggregatedStatus {
|
||||
if (typeof value === 'string' && KNOWN_AGGREGATED_STATUS.has(value as RoleAggregatedStatus)) {
|
||||
return value as RoleAggregatedStatus
|
||||
}
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function coerceRuntimeStatus(value: unknown): AgentAnimStatus {
|
||||
if (value === 'reflecting' || value === 'tool_active' || value === 'idle') return value
|
||||
return 'idle'
|
||||
}
|
||||
|
||||
function mapBackendRoleWorkItemActivitySection(raw: any): RoleWorkItemActivitySection {
|
||||
const kind = typeof raw.kind === 'string' && raw.kind.trim() ? raw.kind : 'activity'
|
||||
const title = typeof raw.title === 'string' && raw.title.trim()
|
||||
? raw.title
|
||||
: kind.replace(/_/g, ' ')
|
||||
return {
|
||||
kind,
|
||||
title,
|
||||
roleName: typeof raw.role_name === 'string'
|
||||
? raw.role_name
|
||||
: (typeof raw.roleName === 'string' ? raw.roleName : undefined),
|
||||
runtimeTaskId: typeof raw.runtime_task_id === 'string'
|
||||
? raw.runtime_task_id
|
||||
: (typeof raw.runtimeTaskId === 'string' ? raw.runtimeTaskId : undefined),
|
||||
entries: mapBackendProgressLog(raw.entries),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBackendRoleWorkItemRow(raw: any): RoleWorkItemRow {
|
||||
const rawActivitySections: unknown[] = Array.isArray(raw.activity_sections)
|
||||
? raw.activity_sections
|
||||
: Array.isArray(raw.activitySections)
|
||||
? raw.activitySections
|
||||
: []
|
||||
return {
|
||||
workItemId: typeof raw.work_item_id === 'string' ? raw.work_item_id : (raw.workItemId ?? ''),
|
||||
workItemProjectionId: typeof raw.work_item_projection_id === 'string'
|
||||
? raw.work_item_projection_id
|
||||
: (raw.workItemProjectionId ?? undefined),
|
||||
phase: coercePhase(raw.phase),
|
||||
kanbanColumn: typeof raw.kanban_column === 'string'
|
||||
? raw.kanban_column
|
||||
: (raw.kanbanColumn ?? 'todo'),
|
||||
title: typeof raw.title === 'string' ? raw.title : '',
|
||||
kind: typeof raw.kind === 'string' ? raw.kind : (raw.kind ?? undefined),
|
||||
isReviewTarget: !!(raw.is_review_target ?? raw.isReviewTarget),
|
||||
executorRoleId: raw.executor_role_id ?? raw.executorRoleId ?? undefined,
|
||||
executorRoleName: raw.executor_role_name ?? raw.executorRoleName ?? undefined,
|
||||
reviewerRoleId: raw.reviewer_role_id ?? raw.reviewerRoleId ?? undefined,
|
||||
createdAt: normalizeEpochMs(raw.created_at ?? raw.createdAt),
|
||||
updatedAt: normalizeEpochMs(raw.updated_at ?? raw.updatedAt),
|
||||
executionTurnId: raw.execution_turn_id ?? raw.executionTurnId ?? undefined,
|
||||
activitySections: rawActivitySections
|
||||
.filter((section): section is Record<string, unknown> => !!section && typeof section === 'object')
|
||||
.map(mapBackendRoleWorkItemActivitySection),
|
||||
progressLog: mapBackendProgressLog(raw.progress_log ?? raw.progressLog),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBackendRoleWorkItems(raw: any): Record<string, RoleWorkItemSummary> | undefined {
|
||||
if (!raw || typeof raw !== 'object') return undefined
|
||||
const out: Record<string, RoleWorkItemSummary> = {}
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (!value || typeof value !== 'object') continue
|
||||
const summary = value as Record<string, unknown>
|
||||
const workItemsRaw = Array.isArray(summary.work_items)
|
||||
? summary.work_items
|
||||
: Array.isArray(summary.workItems)
|
||||
? summary.workItems
|
||||
: []
|
||||
const workItems = (workItemsRaw as any[])
|
||||
.map(mapBackendRoleWorkItemRow)
|
||||
// Defensive ASC sort by createdAt — backend already orders, but a
|
||||
// belt-and-braces sort here means UI never sees a flapping row order
|
||||
// if backend ordering ever drifts.
|
||||
.sort((a, b) => a.createdAt - b.createdAt)
|
||||
const roleKey = typeof summary.role_key === 'string'
|
||||
? summary.role_key
|
||||
: (typeof summary.roleKey === 'string' ? summary.roleKey : key)
|
||||
out[key] = {
|
||||
roleKey,
|
||||
roleId: typeof summary.role_id === 'string'
|
||||
? summary.role_id
|
||||
: (typeof summary.roleId === 'string' ? summary.roleId : key),
|
||||
roleName: typeof summary.role_name === 'string'
|
||||
? summary.role_name
|
||||
: (typeof summary.roleName === 'string' ? summary.roleName : key),
|
||||
roleSessionId: typeof summary.role_session_id === 'string'
|
||||
? summary.role_session_id
|
||||
: (typeof summary.roleSessionId === 'string' ? summary.roleSessionId : undefined),
|
||||
teamInstanceId: typeof summary.team_instance_id === 'string'
|
||||
? summary.team_instance_id
|
||||
: (typeof summary.teamInstanceId === 'string' ? summary.teamInstanceId : undefined),
|
||||
runtimeStatus: coerceRuntimeStatus(summary.runtime_status ?? summary.runtimeStatus),
|
||||
aggregatedStatus: coerceAggregatedStatus(summary.aggregated_status ?? summary.aggregatedStatus),
|
||||
workItems,
|
||||
}
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined
|
||||
}
|
||||
|
||||
export function mapBackendSession(raw: any): Session {
|
||||
const agentStatus = normalizeAgentRuntimeStatus(raw.status, raw.agent_status ?? raw.agentStatus)
|
||||
const taskId = raw.task_id ?? raw.taskId ?? ''
|
||||
const runtimeTaskId = raw.runtime_task_id ?? raw.runtimeTaskId ?? raw.execution_turn_id ?? raw.executionTurnId ?? taskId
|
||||
const executionTurnId = raw.execution_turn_id ?? raw.executionTurnId ?? runtimeTaskId
|
||||
const executionMode = raw.execution_mode ?? raw.executionMode
|
||||
const rawExecMode = String(raw.exec_mode ?? raw.execMode ?? '').trim().toLowerCase()
|
||||
const rawProjectionId = raw.work_item_projection_id ?? raw.workItemProjectionId
|
||||
const isTaskModeRuntime = (
|
||||
rawExecMode === 'task'
|
||||
|| rawExecMode === 'project'
|
||||
|| rawExecMode === 'single'
|
||||
|| executionMode === 'task_mode'
|
||||
|| rawProjectionId === 'task_mode_execution'
|
||||
)
|
||||
const parentSessionId = isTaskModeRuntime
|
||||
? undefined
|
||||
: (raw.parent_session_id ?? raw.parentSessionId)
|
||||
const mapped: Session = {
|
||||
projectId: raw.project_id ?? raw.projectId ?? '',
|
||||
taskId,
|
||||
runtimeTaskId,
|
||||
executionTurnId,
|
||||
channelId: raw.channel_id ?? raw.channelId ?? '',
|
||||
sessionId: raw.session_id ?? raw.sessionId,
|
||||
parentSessionId,
|
||||
mode: (isTaskModeRuntime ? 'primary' : (raw.mode ?? (parentSessionId ? 'child' : 'primary'))) as SessionMode,
|
||||
title: raw.title ?? 'Untitled',
|
||||
status: raw.status ?? 'pending',
|
||||
columnId: raw.column_id ?? raw.columnId ?? 'todo',
|
||||
assigneeIds: raw.assignee_ids ?? raw.assigneeIds ?? [],
|
||||
priority: raw.priority ?? null,
|
||||
tags: raw.tags ?? [],
|
||||
agentStatus,
|
||||
currentTool: raw.current_tool ?? raw.currentTool,
|
||||
displayTool: raw.display_tool ?? raw.displayTool ?? raw.current_tool ?? raw.currentTool,
|
||||
toolElapsedMs: raw.tool_elapsed_ms ?? raw.toolElapsedMs,
|
||||
lastToolSummary: raw.last_tool_summary ?? raw.lastToolSummary,
|
||||
contextTokens: raw.context_tokens ?? raw.contextTokens,
|
||||
contextWindow: raw.context_window ?? raw.contextWindow,
|
||||
contextRemainingPct: raw.context_remaining_pct ?? raw.contextRemainingPct,
|
||||
inputTokens: raw.input_tokens ?? raw.inputTokens,
|
||||
outputTokens: raw.output_tokens ?? raw.outputTokens,
|
||||
totalTokens: raw.total_tokens ?? raw.totalTokens,
|
||||
turnCostUsd: raw.turn_cost_usd ?? raw.turnCostUsd,
|
||||
sessionCostUsd: raw.session_cost_usd ?? raw.sessionCostUsd,
|
||||
pendingPermissionCount: raw.pending_permission_count ?? raw.pendingPermissionCount,
|
||||
drainMode: raw.drain_mode ?? raw.drainMode,
|
||||
progressLog: mapBackendProgressLog(raw.progress_log ?? raw.progressLog),
|
||||
createdAt: typeof raw.created_at === 'number' ? raw.created_at * 1000 : (raw.createdAt ?? Date.now()),
|
||||
updatedAt: typeof raw.updated_at === 'number' ? raw.updated_at * 1000 : (raw.updatedAt ?? Date.now()),
|
||||
messageCount: raw.message_count ?? raw.messageCount ?? 0,
|
||||
latestPreview: raw.latest_preview ?? raw.latestPreview,
|
||||
latestSender: raw.latest_sender ?? raw.latestSender,
|
||||
latestMessageId: raw.latest_message_id ?? raw.latestMessageId,
|
||||
indexLoaded: raw.index_loaded ?? raw.indexLoaded,
|
||||
detailLoaded: raw.detail_loaded ?? raw.detailLoaded,
|
||||
fullLoaded: raw.full_loaded ?? raw.fullLoaded,
|
||||
hasMore: raw.has_more ?? raw.hasMore,
|
||||
detailLoading: raw.detail_loading ?? raw.detailLoading,
|
||||
detailError: raw.detail_error ?? raw.detailError,
|
||||
viewGeneration: raw.view_generation ?? raw.viewGeneration,
|
||||
execMode: raw.exec_mode ?? raw.execMode,
|
||||
companyProfile: isTaskModeRuntime ? undefined : (raw.company_profile ?? raw.companyProfile),
|
||||
orgId: isTaskModeRuntime ? undefined : (raw.org_id ?? raw.organization_id ?? raw.orgId ?? raw.organizationId),
|
||||
preferredAgent: raw.preferred_agent ?? raw.preferredAgent,
|
||||
// Company Mode metadata
|
||||
workItemProjectionId: isTaskModeRuntime ? undefined : rawProjectionId,
|
||||
workItemTurnType: isTaskModeRuntime ? undefined : (raw.work_item_turn_type ?? raw.workItemTurnType),
|
||||
workItemRoleId: isTaskModeRuntime ? undefined : (raw.work_item_role_id ?? raw.workItemRoleId),
|
||||
workItemRoleName: isTaskModeRuntime ? undefined : (raw.work_item_role_name ?? raw.workItemRoleName),
|
||||
workItemGate: isTaskModeRuntime ? undefined : _mapWorkItemGate(raw.work_item_gate ?? raw.workItemGate),
|
||||
employeeAssignment: _mapEmployeeAssignment(raw.employee_assignment ?? raw.employeeAssignment),
|
||||
selectedExecutionAgent: raw.selected_execution_agent ?? raw.selectedExecutionAgent,
|
||||
originChannel: raw.origin_channel ?? raw.originChannel,
|
||||
originTaskId: raw.origin_task_id ?? raw.originTaskId ?? raw.task_id ?? raw.taskId,
|
||||
runtimeControlState: raw.runtime_control_state ?? raw.runtimeControlState,
|
||||
canStop: raw.can_stop ?? raw.canStop,
|
||||
canResume: raw.can_resume ?? raw.canResume,
|
||||
resumeParentTaskId: raw.resume_parent_task_id ?? raw.resumeParentTaskId,
|
||||
resumeParentSessionId: raw.resume_parent_session_id ?? raw.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: raw.pending_runtime_checkpoint_id ?? raw.pendingRuntimeCheckpointId,
|
||||
stopIntentId: raw.stop_intent_id ?? raw.stopIntentId,
|
||||
handoffContext: raw.handoff_context ?? raw.handoffContext,
|
||||
handoffTo: raw.handoff_to ?? raw.handoffTo,
|
||||
artifacts: raw.artifacts,
|
||||
isCompanyRuntime: isTaskModeRuntime ? false : (raw.is_company_runtime ?? raw.isCompanyRuntime),
|
||||
workItemLog: isTaskModeRuntime ? [] : mapBackendWorkItemLog(raw.work_item_log ?? raw.workItemLog),
|
||||
roleWorkItems: isTaskModeRuntime ? undefined : mapBackendRoleWorkItems(raw.role_work_items ?? raw.roleWorkItems),
|
||||
executorRoleWorkItems: isTaskModeRuntime
|
||||
? undefined
|
||||
: mapBackendRoleWorkItems(raw.executor_role_work_items ?? raw.executorRoleWorkItems),
|
||||
draftTurnId: raw.draft_turn_id ?? raw.draftTurnId,
|
||||
runtimeSessionId: raw.runtime_session_id ?? raw.runtimeSessionId,
|
||||
resumeCursor: raw.resume_cursor ?? raw.resumeCursor,
|
||||
activeSubagents: raw.active_subagents ?? raw.activeSubagents,
|
||||
permissionRequests: raw.permission_requests ?? raw.permissionRequests,
|
||||
worktreePath: raw.worktree_path ?? raw.worktreePath,
|
||||
residentStatus: raw.resident_status ?? raw.residentStatus,
|
||||
actionableInboxCount: raw.actionable_inbox_count ?? raw.actionableInboxCount,
|
||||
protocolBacklogCount: raw.protocol_backlog_count ?? raw.protocolBacklogCount,
|
||||
notificationBacklogCount: raw.notification_backlog_count ?? raw.notificationBacklogCount,
|
||||
latestNotification: raw.latest_notification ?? raw.latestNotification,
|
||||
}
|
||||
return canonicalizeSessionExecutionIdentity(mapped)
|
||||
}
|
||||
|
||||
export interface CollabSyncData {
|
||||
channels: ChatChannel[]
|
||||
messages: ChatMessage[]
|
||||
boards: KanbanBoard[]
|
||||
columns: KanbanColumn[]
|
||||
tasks: KanbanTask[]
|
||||
sessions: Session[]
|
||||
}
|
||||
|
||||
export function mapCollabSyncPayload(payload: any): CollabSyncData {
|
||||
const channels = (payload.channels ?? []).map(mapBackendChannel)
|
||||
const messages = (payload.messages ?? []).map(mapBackendMessage)
|
||||
const boards = (payload.boards ?? []).map(mapBackendBoard)
|
||||
const columns = (payload.columns ?? []).map(mapBackendColumn)
|
||||
const sessions = hydrateCompanyRuntimeSessions(
|
||||
(payload.sessions ?? []).map(mapBackendSession),
|
||||
messages,
|
||||
)
|
||||
const tasks = hydrateCompanyRuntimeTasks(
|
||||
(payload.tasks ?? []).map(mapBackendTask),
|
||||
sessions,
|
||||
)
|
||||
|
||||
return {
|
||||
channels,
|
||||
messages,
|
||||
boards,
|
||||
columns,
|
||||
tasks,
|
||||
sessions,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user