181 lines
4.7 KiB
TypeScript
181 lines
4.7 KiB
TypeScript
import { existsSync, readFileSync, unlinkSync } from "node:fs"
|
|
import { z } from "zod"
|
|
import { acquireLock, ensureDir, writeJsonAtomic } from "../../features/claude-tasks/storage"
|
|
import { getTeamInboxDir, getTeamInboxPath } from "./paths"
|
|
import { validateAgentNameOrLead, validateTeamName } from "./name-validation"
|
|
import { TeamInboxMessage, TeamInboxMessageSchema } from "./types"
|
|
|
|
const TeamInboxListSchema = z.array(TeamInboxMessageSchema)
|
|
|
|
function nowIso(): string {
|
|
return new Date().toISOString()
|
|
}
|
|
|
|
function assertValidTeamName(teamName: string): void {
|
|
const validationError = validateTeamName(teamName)
|
|
if (validationError) {
|
|
throw new Error(validationError)
|
|
}
|
|
}
|
|
|
|
function assertValidInboxAgentName(agentName: string): void {
|
|
const validationError = validateAgentNameOrLead(agentName)
|
|
if (validationError) {
|
|
throw new Error(validationError)
|
|
}
|
|
}
|
|
|
|
function withInboxLock<T>(teamName: string, operation: () => T): T {
|
|
assertValidTeamName(teamName)
|
|
const inboxDir = getTeamInboxDir(teamName)
|
|
ensureDir(inboxDir)
|
|
const lock = acquireLock(inboxDir)
|
|
if (!lock.acquired) {
|
|
throw new Error("inbox_lock_unavailable")
|
|
}
|
|
|
|
try {
|
|
return operation()
|
|
} finally {
|
|
lock.release()
|
|
}
|
|
}
|
|
|
|
function parseInboxFile(content: string): TeamInboxMessage[] {
|
|
try {
|
|
const parsed = JSON.parse(content)
|
|
const result = TeamInboxListSchema.safeParse(parsed)
|
|
return result.success ? result.data : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
function readInboxMessages(teamName: string, agentName: string): TeamInboxMessage[] {
|
|
assertValidTeamName(teamName)
|
|
assertValidInboxAgentName(agentName)
|
|
const path = getTeamInboxPath(teamName, agentName)
|
|
if (!existsSync(path)) {
|
|
return []
|
|
}
|
|
return parseInboxFile(readFileSync(path, "utf-8"))
|
|
}
|
|
|
|
function writeInboxMessages(teamName: string, agentName: string, messages: TeamInboxMessage[]): void {
|
|
assertValidTeamName(teamName)
|
|
assertValidInboxAgentName(agentName)
|
|
const path = getTeamInboxPath(teamName, agentName)
|
|
writeJsonAtomic(path, messages)
|
|
}
|
|
|
|
export function ensureInbox(teamName: string, agentName: string): void {
|
|
assertValidTeamName(teamName)
|
|
assertValidInboxAgentName(agentName)
|
|
withInboxLock(teamName, () => {
|
|
const path = getTeamInboxPath(teamName, agentName)
|
|
if (!existsSync(path)) {
|
|
writeJsonAtomic(path, [])
|
|
}
|
|
})
|
|
}
|
|
|
|
export function appendInboxMessage(teamName: string, agentName: string, message: TeamInboxMessage): void {
|
|
assertValidTeamName(teamName)
|
|
assertValidInboxAgentName(agentName)
|
|
withInboxLock(teamName, () => {
|
|
const path = getTeamInboxPath(teamName, agentName)
|
|
const messages = existsSync(path) ? parseInboxFile(readFileSync(path, "utf-8")) : []
|
|
messages.push(TeamInboxMessageSchema.parse(message))
|
|
writeInboxMessages(teamName, agentName, messages)
|
|
})
|
|
}
|
|
|
|
export function clearInbox(teamName: string, agentName: string): void {
|
|
assertValidTeamName(teamName)
|
|
assertValidInboxAgentName(agentName)
|
|
withInboxLock(teamName, () => {
|
|
const path = getTeamInboxPath(teamName, agentName)
|
|
if (existsSync(path)) {
|
|
unlinkSync(path)
|
|
}
|
|
})
|
|
}
|
|
|
|
export function sendPlainInboxMessage(
|
|
teamName: string,
|
|
from: string,
|
|
to: string,
|
|
text: string,
|
|
summary: string,
|
|
color?: string,
|
|
): void {
|
|
appendInboxMessage(teamName, to, {
|
|
from,
|
|
text,
|
|
timestamp: nowIso(),
|
|
read: false,
|
|
summary,
|
|
...(color ? { color } : {}),
|
|
})
|
|
}
|
|
|
|
export function sendStructuredInboxMessage(
|
|
teamName: string,
|
|
from: string,
|
|
to: string,
|
|
payload: Record<string, unknown>,
|
|
summary?: string,
|
|
): void {
|
|
appendInboxMessage(teamName, to, {
|
|
from,
|
|
text: JSON.stringify(payload),
|
|
timestamp: nowIso(),
|
|
read: false,
|
|
...(summary ? { summary } : {}),
|
|
})
|
|
}
|
|
|
|
export function readInbox(
|
|
teamName: string,
|
|
agentName: string,
|
|
unreadOnly = false,
|
|
markAsRead = true,
|
|
): TeamInboxMessage[] {
|
|
return withInboxLock(teamName, () => {
|
|
const messages = readInboxMessages(teamName, agentName)
|
|
|
|
const selected = unreadOnly ? messages.filter((message) => !message.read) : [...messages]
|
|
|
|
if (!markAsRead || selected.length === 0) {
|
|
return selected
|
|
}
|
|
|
|
const selectedSet = unreadOnly ? new Set(selected) : null
|
|
let changed = false
|
|
|
|
const updated = messages.map((message) => {
|
|
if (!unreadOnly) {
|
|
if (!message.read) {
|
|
changed = true
|
|
}
|
|
return { ...message, read: true }
|
|
}
|
|
|
|
if (selectedSet?.has(message)) {
|
|
changed = true
|
|
return { ...message, read: true }
|
|
}
|
|
return message
|
|
})
|
|
|
|
if (changed) {
|
|
writeInboxMessages(teamName, agentName, updated)
|
|
}
|
|
return selected
|
|
})
|
|
}
|
|
|
|
export function buildShutdownRequestId(recipient: string): string {
|
|
return `shutdown-${Date.now()}@${recipient}`
|
|
}
|