This commit is contained in:
ZacharyZhang-NY committed 2026-04-04 17:01:26 -04:00
1 parent 67c757f481
commit b8dda3c8d0
77 files changed
+1577 -2808

No files matched your search

+76 -27
View File
@@ -100,7 +100,7 @@ export function registerMiscRoutes(server: AgentApiServer) {
const onBrowserAction = server.onBrowserAction_;
if (onBrowserAction) {
onBrowserAction(actionId, companyId, input.socialAccountId, input.actionType, input.payload as Record<string, unknown>);
onBrowserAction(actionId, companyId);
}
server.json(res, 201, { id: actionId, status: "queued", message: "Browser action queued for execution" });
@@ -183,10 +183,25 @@ export function registerMiscRoutes(server: AgentApiServer) {
server.json(res, 403, { error: "Company mismatch" });
return;
}
const data = body as Record<string, unknown>;
const participantAgentIds = data.participantAgentIds ? String(data.participantAgentIds) : "[]";
const meetingCreateSchema = z.object({
type: z.string().default("department_sync"),
title: z.string().default("Meeting"),
participantAgentIds: z.string().default("[]"),
scheduledAt: z.string().default(new Date().toISOString()),
durationMinutes: z.number().default(30),
agendaJson: z.string().default("[]"),
});
const parsed = meetingCreateSchema.safeParse(body);
if (!parsed.success) {
server.json(res, 400, { error: "Invalid meeting payload", details: parsed.error.issues });
return;
}
const input = parsed.data;
try {
const parsedParticipants = JSON.parse(participantAgentIds) as string[];
const parsedParticipants = JSON.parse(input.participantAgentIds) as string[];
for (const agentId of parsedParticipants) {
if (typeof agentId !== "string" || !db.belongsToCompany("agents", agentId, token.companyId)) {
server.json(res, 400, { error: "Meeting participants must belong to the current company" });
@@ -199,13 +214,13 @@ export function registerMiscRoutes(server: AgentApiServer) {
}
const id = db.saveMeeting({
companyId: token.companyId,
type: String(data.type ?? "department_sync"),
title: String(data.title ?? "Meeting"),
type: input.type,
title: input.title,
organizerAgentId: token.agentId,
participantAgentIds,
scheduledAt: String(data.scheduledAt ?? new Date().toISOString()),
durationMinutes: Number(data.durationMinutes ?? 30),
agendaJson: data.agendaJson ? String(data.agendaJson) : "[]",
participantAgentIds: input.participantAgentIds,
scheduledAt: input.scheduledAt,
durationMinutes: input.durationMinutes,
agendaJson: input.agendaJson,
status: "scheduled",
});
onDomainChanged();
@@ -229,11 +244,28 @@ export function registerMiscRoutes(server: AgentApiServer) {
server.json(res, 404, { error: "Meeting not found" });
return;
}
const data = body as Record<string, unknown>;
const meetingUpdateSchema = z.object({
type: z.string().optional(),
title: z.string().optional(),
notesJson: z.string().optional(),
decisionsJson: z.string().optional(),
actionItemsJson: z.string().optional(),
status: z.string().optional(),
scheduledAt: z.string().optional(),
});
const parsed = meetingUpdateSchema.safeParse(body);
if (!parsed.success) {
server.json(res, 400, { error: "Invalid meeting update payload", details: parsed.error.issues });
return;
}
const data = parsed.data;
let actionItems: Array<{ title?: string; assigneeAgentId?: string; description?: string }> = [];
if (data.actionItemsJson) {
try {
actionItems = JSON.parse(String(data.actionItemsJson)) as Array<{ title?: string; assigneeAgentId?: string; description?: string }>;
actionItems = JSON.parse(data.actionItemsJson) as Array<{ title?: string; assigneeAgentId?: string; description?: string }>;
} catch {
server.json(res, 400, { error: "actionItemsJson must be valid JSON" });
return;
@@ -248,13 +280,13 @@ export function registerMiscRoutes(server: AgentApiServer) {
db.saveMeeting({
id: params.meetingId,
companyId: token.companyId,
type: data.type ? String(data.type) : "department_sync",
title: data.title ? String(data.title) : "Meeting",
notesJson: data.notesJson ? String(data.notesJson) : undefined,
decisionsJson: data.decisionsJson ? String(data.decisionsJson) : undefined,
actionItemsJson: data.actionItemsJson ? String(data.actionItemsJson) : undefined,
status: data.status ? String(data.status) : undefined,
scheduledAt: data.scheduledAt ? String(data.scheduledAt) : new Date().toISOString(),
type: data.type ?? "department_sync",
title: data.title ?? "Meeting",
notesJson: data.notesJson,
decisionsJson: data.decisionsJson,
actionItemsJson: data.actionItemsJson,
status: data.status,
scheduledAt: data.scheduledAt ?? new Date().toISOString(),
});
onDomainChanged();
// When action items are added, create tasks for them automatically
@@ -291,17 +323,34 @@ export function registerMiscRoutes(server: AgentApiServer) {
server.json(res, 403, { error: "Company mismatch" });
return;
}
const data = body as Record<string, unknown>;
const knowledgeCreateSchema = z.object({
category: z.string().default("lesson_learned"),
topic: z.string().default(""),
content: z.string().default(""),
importance: z.string().default("medium"),
tagsJson: z.string().default("[]"),
referencedEntityType: z.string().nullable().optional(),
referencedEntityId: z.string().nullable().optional(),
});
const parsed = knowledgeCreateSchema.safeParse(body);
if (!parsed.success) {
server.json(res, 400, { error: "Invalid knowledge entry payload", details: parsed.error.issues });
return;
}
const input = parsed.data;
const id = db.saveKnowledgeEntry({
companyId: token.companyId,
category: String(data.category ?? "lesson_learned"),
topic: String(data.topic ?? ""),
content: String(data.content ?? ""),
category: input.category,
topic: input.topic,
content: input.content,
authorAgentId: token.agentId,
importance: String(data.importance ?? "medium"),
tagsJson: data.tagsJson ? String(data.tagsJson) : "[]",
referencedEntityType: data.referencedEntityType ? String(data.referencedEntityType) : null,
referencedEntityId: data.referencedEntityId ? String(data.referencedEntityId) : null,
importance: input.importance,
tagsJson: input.tagsJson,
referencedEntityType: input.referencedEntityType ?? null,
referencedEntityId: input.referencedEntityId ?? null,
sourceRunId: token.runId ?? null,
});
onDomainChanged();
+3 -3
View File
@@ -67,7 +67,7 @@ export class AgentApiServer {
private emitEventCb: (event: DesktopEvent) => void;
private callbacks: ApiServerEventCallbacks;
private allowedOrigins: Set<string>;
private onBrowserActionCb: ((actionId: string, companyId: string, socialAccountId: string, actionType: string, payload: Record<string, unknown>) => void) | null = null;
private onBrowserActionCb: ((actionId: string, companyId: string) => void) | null = null;
constructor(
db: AppDatabase,
@@ -97,7 +97,7 @@ export class AgentApiServer {
};
}
setBrowserActionHandler(handler: (actionId: string, companyId: string, socialAccountId: string, actionType: string, payload: Record<string, unknown>) => void) {
setBrowserActionHandler(handler: (actionId: string, companyId: string) => void) {
this.onBrowserActionCb = handler;
}
@@ -269,7 +269,7 @@ export class AgentApiServer {
get onDomainChanged_(): () => void { return this.onDomainChangedCb; }
get onWakeAgent_(): (agentId: string, companyId: string, trigger: string) => string | null { return this.onWakeAgentCb; }
get emitEvent_(): (event: DesktopEvent) => void { return this.emitEventCb; }
get onBrowserAction_(): ((actionId: string, companyId: string, socialAccountId: string, actionType: string, payload: Record<string, unknown>) => void) | null {
get onBrowserAction_(): ((actionId: string, companyId: string) => void) | null {
return this.onBrowserActionCb;
}
get port_(): number { return this.port; }
+5
View File
@@ -127,11 +127,16 @@ export function createBrowserActionService({
browserManagerRef.current = new BrowserManager(join(db.artifactsDir, "browser"));
}
async function extractProfileFromSession(socialAccountId: string, platform: string) {
return requireBrowserManager().extractProfileFromSession(socialAccountId, platform);
}
return {
dispatchBrowserAction,
clearSocialAccountData,
cancelAction,
launchLoginBrowser,
extractProfileFromSession,
resetBrowserManager,
};
}
+27
View File
@@ -231,6 +231,33 @@ export class BrowserManager {
}
}
async extractProfileFromSession(socialAccountId: string, platform: string): Promise<{ accountName: string; displayName: string }> {
const ses = this.getSession(socialAccountId);
const win = new BrowserWindow({
show: false,
webPreferences: { session: ses, nodeIntegration: false, contextIsolation: true, sandbox: true },
});
try {
const rules = getPlatformRules(platform);
await win.loadURL(rules.authCheckUrl);
await waitForLoad(win, 12000);
const result = await win.webContents.executeJavaScript(`
(() => {
const meta = document.querySelector('meta[property="al:ios:url"], meta[name="user-login"], meta[property="profile:username"]');
if (meta) return { accountName: meta.getAttribute("content") || "", displayName: "" };
const title = document.title || "";
return { accountName: "", displayName: title.split(/[|–—\\-/]/).map(s => s.trim()).find(s => s.length > 0 && s.length < 40) || "" };
})()
`);
return result && (result.accountName || result.displayName) ? result : { accountName: "", displayName: "" };
} catch {
return { accountName: "", displayName: "" };
} finally {
if (!win.isDestroyed()) win.close();
}
}
shutdown() {
for (const [, win] of this.loginWindows) {
if (!win.isDestroyed()) win.close();
+77
View File
@@ -85,6 +85,83 @@ export const PLATFORM_RULES: Record<string, PlatformRule> = {
cookieOrigins: ["https://bsky.app"],
unauthenticatedUrlMarkers: ["/login"],
},
discord: {
loginUrl: "https://discord.com/login",
authCheckUrl: "https://discord.com/channels/@me",
allowedHosts: ["discord.com"],
cookieOrigins: ["https://discord.com"],
unauthenticatedUrlMarkers: ["/login"],
},
threads: {
loginUrl: "https://www.threads.net/login",
authCheckUrl: "https://www.threads.net/",
allowedHosts: ["threads.net"],
cookieOrigins: ["https://www.threads.net"],
unauthenticatedUrlMarkers: ["/login"],
},
pinterest: {
loginUrl: "https://www.pinterest.com/login/",
authCheckUrl: "https://www.pinterest.com/",
allowedHosts: ["pinterest.com"],
cookieOrigins: ["https://www.pinterest.com"],
unauthenticatedUrlMarkers: ["/login"],
},
telegram: {
loginUrl: "https://web.telegram.org/",
authCheckUrl: "https://web.telegram.org/",
allowedHosts: ["telegram.org", "t.me"],
cookieOrigins: ["https://web.telegram.org"],
unauthenticatedUrlMarkers: ["/auth"],
},
whatsapp: {
loginUrl: "https://web.whatsapp.com/",
authCheckUrl: "https://web.whatsapp.com/",
allowedHosts: ["whatsapp.com"],
cookieOrigins: ["https://web.whatsapp.com"],
unauthenticatedUrlMarkers: [],
},
wechat: {
loginUrl: "https://mp.weixin.qq.com/",
authCheckUrl: "https://mp.weixin.qq.com/",
allowedHosts: ["mp.weixin.qq.com", "weixin.qq.com"],
cookieOrigins: ["https://mp.weixin.qq.com"],
unauthenticatedUrlMarkers: ["/cgi-bin/loginpage"],
},
weibo: {
loginUrl: "https://weibo.com/login.php",
authCheckUrl: "https://weibo.com/",
allowedHosts: ["weibo.com", "weibo.cn"],
cookieOrigins: ["https://weibo.com"],
unauthenticatedUrlMarkers: ["/login", "/signup"],
},
xiaohongshu: {
loginUrl: "https://www.xiaohongshu.com/explore",
authCheckUrl: "https://www.xiaohongshu.com/",
allowedHosts: ["xiaohongshu.com"],
cookieOrigins: ["https://www.xiaohongshu.com"],
unauthenticatedUrlMarkers: ["/login"],
},
douyin: {
loginUrl: "https://www.douyin.com/",
authCheckUrl: "https://www.douyin.com/",
allowedHosts: ["douyin.com"],
cookieOrigins: ["https://www.douyin.com"],
unauthenticatedUrlMarkers: [],
},
bilibili: {
loginUrl: "https://passport.bilibili.com/login",
authCheckUrl: "https://www.bilibili.com/",
allowedHosts: ["bilibili.com"],
cookieOrigins: ["https://www.bilibili.com", "https://passport.bilibili.com"],
unauthenticatedUrlMarkers: ["/login"],
},
zhihu: {
loginUrl: "https://www.zhihu.com/signin",
authCheckUrl: "https://www.zhihu.com/",
allowedHosts: ["zhihu.com"],
cookieOrigins: ["https://www.zhihu.com"],
unauthenticatedUrlMarkers: ["/signin", "/sign-in"],
},
other: {
loginUrl: "https://google.com",
authCheckUrl: "https://google.com",
+28
View File
@@ -71,6 +71,17 @@ export function getComposeUrl(platform: string): string {
bluesky: "https://bsky.app/",
youtube: "https://www.youtube.com/",
tiktok: "https://www.tiktok.com/",
discord: "https://discord.com/channels/@me",
threads: "https://www.threads.net/",
pinterest: "https://www.pinterest.com/pin-creation-tool/",
telegram: "https://web.telegram.org/",
whatsapp: "https://web.whatsapp.com/",
wechat: "https://mp.weixin.qq.com/",
weibo: "https://weibo.com/",
xiaohongshu: "https://www.xiaohongshu.com/explore",
douyin: "https://www.douyin.com/",
bilibili: "https://www.bilibili.com/",
zhihu: "https://www.zhihu.com/question/waiting",
};
return urls[platform] || getPlatformRules(platform).loginUrl;
}
@@ -85,6 +96,17 @@ export function getFeedUrl(platform: string): string {
bluesky: "https://bsky.app/",
youtube: "https://www.youtube.com/",
tiktok: "https://www.tiktok.com/",
discord: "https://discord.com/channels/@me",
threads: "https://www.threads.net/",
pinterest: "https://www.pinterest.com/",
telegram: "https://web.telegram.org/",
whatsapp: "https://web.whatsapp.com/",
wechat: "https://mp.weixin.qq.com/",
weibo: "https://weibo.com/",
xiaohongshu: "https://www.xiaohongshu.com/explore",
douyin: "https://www.douyin.com/",
bilibili: "https://www.bilibili.com/",
zhihu: "https://www.zhihu.com/",
};
return urls[platform] || getPlatformRules(platform).loginUrl;
}
@@ -95,6 +117,12 @@ export function getSearchUrl(platform: string, query: string): string {
twitter: `https://x.com/search?q=${q}`,
linkedin: `https://www.linkedin.com/search/results/all/?keywords=${q}`,
reddit: `https://www.reddit.com/search/?q=${q}`,
discord: `https://discord.com/channels/@me`,
pinterest: `https://www.pinterest.com/search/pins/?q=${q}`,
weibo: `https://s.weibo.com/weibo?q=${q}`,
xiaohongshu: `https://www.xiaohongshu.com/search_result?keyword=${q}`,
bilibili: `https://search.bilibili.com/all?keyword=${q}`,
zhihu: `https://www.zhihu.com/search?type=content&q=${q}`,
};
return urls[platform] || `https://google.com/search?q=${q}`;
}
+4 -6
View File
@@ -27,18 +27,16 @@ export interface BudgetGateResult {
function getScopeLabel(db: AppDatabase, policy: BudgetPolicyRecord): string {
if (policy.scopeType === "agent") {
try {
const agents = db.listSnapshot({ companyId: null }).agents;
const agent = agents.find((a) => a.id === policy.scopeId);
return agent ? `Agent "${agent.name}"` : `Agent ${policy.scopeId.slice(0, 8)}`;
const agent = db.getAgent(policy.scopeId);
return `Agent "${agent.name}"`;
} catch {
return `Agent ${policy.scopeId.slice(0, 8)}`;
}
}
if (policy.scopeType === "project") {
try {
const projects = db.listSnapshot({ companyId: null }).projects;
const project = projects.find((p) => p.id === policy.scopeId);
return project ? `Project "${project.name}"` : `Project ${policy.scopeId.slice(0, 8)}`;
const row = db.queryAll<{ name: string }>("select name from projects where id = ?", policy.scopeId);
return row.length > 0 ? `Project "${row[0].name}"` : `Project ${policy.scopeId.slice(0, 8)}`;
} catch {
return `Project ${policy.scopeId.slice(0, 8)}`;
}
+46 -250
View File
@@ -24,6 +24,51 @@ export interface ConnectorDefinition {
listModels?: () => Promise<Array<{ id: string; label: string }>>;
}
const claudeCompatModule: ServerAdapterModule = {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
};
const claudeCompatCapabilities: CapabilityMatrix = {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
};
const COMPAT_SLOT_IDS: ConnectorId[] = [
"claude_compatible",
"claude_compat_2",
"claude_compat_3",
"claude_compat_4",
"claude_compat_5",
"claude_compat_6",
"claude_compat_7",
"claude_compat_8",
"claude_compat_9",
"claude_compat_10",
];
const claudeCompatSlots: ConnectorDefinition[] = COMPAT_SLOT_IDS.map((id, i) => ({
id,
label: i === 0 ? "Claude Code Compatible API" : `Claude Code Compatible API (${i + 1})`,
description: i === 0
? "Claude Code CLI connected to a third-party API provider (e.g. DeepSeek, OpenRouter, Ollama)."
: `Claude Code CLI compatible API slot ${i + 1} for a third-party provider.`,
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: claudeCompatCapabilities,
configurationDoc: claudeDoc,
module: claudeCompatModule,
listModels: async () => claudeModels,
}));
export const CONNECTORS: ConnectorDefinition[] = [
{
id: "codex_local",
@@ -75,256 +120,7 @@ export const CONNECTORS: ConnectorDefinition[] = [
},
listModels: async () => claudeModels,
},
{
id: "claude_compatible",
label: "Claude Code Compatible API",
description: "Claude Code CLI connected to a third-party API provider (e.g. DeepSeek, OpenRouter, Ollama).",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_2",
label: "Claude Code Compatible API (2)",
description: "Second Claude Code CLI compatible API slot for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_3",
label: "Claude Code Compatible API (3)",
description: "Third Claude Code CLI compatible API slot for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_4",
label: "Claude Code Compatible API (4)",
description: "Claude Code CLI compatible API slot 4 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_5",
label: "Claude Code Compatible API (5)",
description: "Claude Code CLI compatible API slot 5 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_6",
label: "Claude Code Compatible API (6)",
description: "Claude Code CLI compatible API slot 6 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_7",
label: "Claude Code Compatible API (7)",
description: "Claude Code CLI compatible API slot 7 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_8",
label: "Claude Code Compatible API (8)",
description: "Claude Code CLI compatible API slot 8 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_9",
label: "Claude Code Compatible API (9)",
description: "Claude Code CLI compatible API slot 9 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
{
id: "claude_compat_10",
label: "Claude Code Compatible API (10)",
description: "Claude Code CLI compatible API slot 10 for a third-party provider.",
defaultCommand: "claude",
defaultModel: null,
notes: "",
capabilityMatrix: {
sessionResume: true,
gracefulCancel: true,
modelDiscovery: false,
structuredTranscript: true,
costAttribution: false,
},
configurationDoc: claudeDoc,
module: {
type: "claude_local",
execute: claudeExecute,
testEnvironment: claudeTestEnvironment,
sessionCodec: claudeSessionCodec,
nativeContextManagement: true,
models: claudeModels,
},
listModels: async () => claudeModels,
},
...claudeCompatSlots,
{
id: "gemini_local",
label: "Gemini CLI",
+12 -3
View File
@@ -89,18 +89,27 @@ export function registerBrowserHandlers({
return fail("ACCOUNT_NOT_FOUND", "Social account not found");
}
browserActions.launchLoginBrowser(input.socialAccountId, account.platform).then((result) => {
browserActions.launchLoginBrowser(input.socialAccountId, account.platform).then(async (result) => {
if (result.success) {
const latestAccount = db.getSocialAccount(input.socialAccountId, account.companyId);
if (!latestAccount) {
return;
}
let detectedName = "";
let detectedDisplay = "";
try {
const profile = await browserActions.extractProfileFromSession(input.socialAccountId, account.platform);
detectedName = profile.accountName;
detectedDisplay = profile.displayName;
} catch {
// Profile extraction is best-effort; proceed with login status update
}
db.saveSocialAccount({
id: latestAccount.id,
companyId: latestAccount.companyId,
platform: latestAccount.platform,
accountName: latestAccount.accountName,
displayName: latestAccount.displayName,
accountName: detectedName || latestAccount.accountName,
displayName: detectedDisplay || latestAccount.displayName,
profileUrl: latestAccount.profileUrl,
credentialSecretId: latestAccount.credentialSecretId,
status: "logged_in",
+5 -7
View File
@@ -17,6 +17,10 @@ interface BrowserActionControl {
screenshotPath: string | null;
error: string | null;
}>;
extractProfileFromSession: (socialAccountId: string, platform: string) => Promise<{
accountName: string;
displayName: string;
}>;
}
export interface RegisterDomainHandlersDependencies {
@@ -48,10 +52,4 @@ export interface RegisterDomainHandlersDependencies {
onHumanMessage?: (companyId: string, toAgentId?: string | null, threadId?: string | null, waitStateId?: string | null) => void;
}
export function ok<T>(data: T): DesktopResult<T> {
return { ok: true, data };
}
export function fail(code: string, message: string): DesktopResult<never> {
return { ok: false, error: { code, message } };
}
export { ok, fail } from "./ipc-core-common";
-129
View File
@@ -77,135 +77,6 @@ export function normalizeMessagesForPrompt(
// Applied when session state is restored, not at every message send.
// ---------------------------------------------------------------------------
/**
* A transcript entry from adapter session state.
* Adapters serialize their conversation as arrays of these blocks.
*/
export interface TranscriptBlock {
role: "user" | "assistant" | "system";
content: string | TranscriptContentBlock[];
}
export interface TranscriptContentBlock {
type: string;
id?: string;
tool_use_id?: string;
name?: string;
input?: unknown;
content?: string;
text?: string;
}
/**
* Repair tool_use / tool_result pairing in a serialized transcript.
* - Orphan tool_results (no matching tool_use) are removed.
* - Unpaired tool_use blocks get a synthetic "[interrupted]" result appended.
* - Duplicate tool_use IDs are deduplicated (keep first).
*
* Returns a repaired copy; does not mutate the input.
*/
export function repairToolPairing(transcript: TranscriptBlock[]): TranscriptBlock[] {
const toolUseIds = new Set<string>();
const toolResultIds = new Set<string>();
// Pass 1: collect all tool_use and tool_result IDs
for (const block of transcript) {
if (!Array.isArray(block.content)) continue;
for (const part of block.content) {
if (part.type === "tool_use" && part.id) {
toolUseIds.add(part.id);
}
if (part.type === "tool_result" && part.tool_use_id) {
toolResultIds.add(part.tool_use_id);
}
}
}
// Pass 2: filter orphan tool_results and deduplicate tool_use IDs
const seenToolUseIds = new Set<string>();
const repaired: TranscriptBlock[] = [];
for (const block of transcript) {
if (typeof block.content === "string") {
repaired.push(block);
continue;
}
const filteredContent: TranscriptContentBlock[] = [];
for (const part of block.content) {
if (part.type === "tool_result" && part.tool_use_id && !toolUseIds.has(part.tool_use_id)) {
continue; // orphan tool_result — remove
}
if (part.type === "tool_use" && part.id) {
if (seenToolUseIds.has(part.id)) continue; // duplicate — remove
seenToolUseIds.add(part.id);
}
filteredContent.push(part);
}
if (filteredContent.length > 0) {
repaired.push({ ...block, content: filteredContent });
}
}
// Pass 3: add synthetic results for unpaired tool_use blocks
const unpairedToolUseIds = [...toolUseIds].filter((id) => !toolResultIds.has(id));
if (unpairedToolUseIds.length > 0) {
const syntheticResults: TranscriptContentBlock[] = unpairedToolUseIds.map((id) => ({
type: "tool_result",
tool_use_id: id,
content: "[interrupted — tool call did not complete before session ended]",
}));
repaired.push({
role: "user",
content: syntheticResults,
});
}
return repaired;
}
/**
* Validate and return repair statistics for a transcript.
* Use for logging/diagnostics without modifying the transcript.
*/
export function diagnoseTranscript(transcript: TranscriptBlock[]): {
totalBlocks: number;
toolUseCount: number;
toolResultCount: number;
orphanResultCount: number;
unpairedUseCount: number;
} {
const toolUseIds = new Set<string>();
const toolResultIds = new Set<string>();
let toolUseCount = 0;
let toolResultCount = 0;
for (const block of transcript) {
if (!Array.isArray(block.content)) continue;
for (const part of block.content) {
if (part.type === "tool_use" && part.id) {
toolUseIds.add(part.id);
toolUseCount++;
}
if (part.type === "tool_result" && part.tool_use_id) {
toolResultIds.add(part.tool_use_id);
toolResultCount++;
}
}
}
const orphanResultCount = [...toolResultIds].filter((id) => !toolUseIds.has(id)).length;
const unpairedUseCount = [...toolUseIds].filter((id) => !toolResultIds.has(id)).length;
return {
totalBlocks: transcript.length,
toolUseCount,
toolResultCount,
orphanResultCount,
unpairedUseCount,
};
}
export interface CommentRecord {
taskId: string;
+1 -1
View File
@@ -55,7 +55,7 @@ const RUBRICS: Record<TaskType, TaskRubric> = {
};
/** Returns the rubric for the given task type, defaulting to "general" for unknown values. */
export function getTaskTypeRubric(taskType: string): TaskRubric {
function getTaskTypeRubric(taskType: string): TaskRubric {
const key = taskType as TaskType;
return RUBRICS[key] ?? RUBRICS.general;
}
@@ -1,154 +1,16 @@
/** Thread Integration — wires thread runtime into orchestration callbacks. */
import type { AppDatabase } from "./database";
import type { ExecutionMode, ThreadType } from "@shared/types";
import type { ExecutionMode } from "@shared/types";
import {
type ThreadIntegrationDeps,
findTaskThread,
ensureTaskThread,
ensureApprovalThread,
announceApprovalToBoard,
notifyThreadSubscribers,
} from "./thread-helpers";
interface ThreadIntegrationDeps {
db: AppDatabase;
logger: { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
emitEvent?: (event: import("@shared/types").DesktopEvent) => void;
publishDomainChanged?: () => void;
notify?: (options: { title: string; body: string }) => void;
}
function findTaskThread(db: AppDatabase, taskId: string, companyId: string) {
const existing = db.listThreads({
companyId,
type: "task" as ThreadType,
relatedEntityType: "task",
relatedEntityId: taskId,
limit: 1,
});
return existing[0] ?? null;
}
function ensureTaskThread(db: AppDatabase, taskId: string, companyId: string, title: string) {
const existing = findTaskThread(db, taskId, companyId);
if (existing) return existing;
return db.createThread({
companyId,
title,
type: "task" as ThreadType,
relatedEntityType: "task",
relatedEntityId: taskId,
});
}
function findApprovalThread(db: AppDatabase, approvalId: string, companyId: string) {
const existing = db.listThreads({
companyId,
type: "approval" as ThreadType,
relatedEntityType: "approval",
relatedEntityId: approvalId,
limit: 1,
});
return existing[0] ?? null;
}
function ensureApprovalThread(db: AppDatabase, approvalId: string, companyId: string, title: string) {
const existing = findApprovalThread(db, approvalId, companyId);
if (existing) return existing;
return db.createThread({
companyId,
title,
type: "approval" as ThreadType,
relatedEntityType: "approval",
relatedEntityId: approvalId,
});
}
function resolveApprovalAnnouncementChannel(
db: AppDatabase,
companyId: string,
taskId: string | null,
requestedByAgentId: string | null,
): { channel: "department" | "project" | "company"; channelTargetId: string | null } | null {
if (requestedByAgentId) {
try {
const requester = db.getAgent(requestedByAgentId);
if (requester.companyId === companyId && requester.department) {
return { channel: "department", channelTargetId: requester.department };
}
} catch {
// Fall through to task/project/company targeting.
}
}
if (taskId) {
try {
const task = db.getTask(taskId);
if (task.companyId === companyId && task.projectId) {
return { channel: "project", channelTargetId: task.projectId };
}
} catch {
// Fall through to company-wide routing.
}
}
return { channel: "company", channelTargetId: null };
}
function announceApprovalToBoard(
db: AppDatabase,
approval: {
id: string;
companyId: string;
relatedTaskId: string | null;
requestedByAgentId: string | null;
payloadSummary: string;
impactSummary: string;
},
deps: Pick<ThreadIntegrationDeps, "emitEvent" | "publishDomainChanged">,
): void {
if (!approval.requestedByAgentId || !deps.emitEvent || !deps.publishDomainChanged) return;
const target = resolveApprovalAnnouncementChannel(
db,
approval.companyId,
approval.relatedTaskId,
approval.requestedByAgentId,
);
if (!target) return;
const messageId = db.sendAgentMessage({
companyId: approval.companyId,
fromAgentId: approval.requestedByAgentId,
channel: target.channel,
channelTargetId: target.channelTargetId,
subject: approval.payloadSummary,
body: `@Board ${approval.impactSummary || approval.payloadSummary}`,
priority: "urgent",
senderKind: "agent",
});
const message = db.getAgentMessage(messageId, approval.companyId);
if (message) {
deps.emitEvent({ type: "new-message", message });
}
deps.publishDomainChanged();
}
/** Check subscriber notifyOn preferences and fire desktop notification. */
function notifyThreadSubscribers(
db: AppDatabase,
threadId: string,
threadTitle: string,
eventBody: string,
notify?: (options: { title: string; body: string }) => void,
): void {
if (!notify) return;
const subscriptions = db.listSubscriptions(threadId);
for (const sub of subscriptions) {
if (sub.subscriberType !== "user") continue;
if (sub.notifyOn === "none") continue;
if (sub.notifyOn === "mentions") {
if (!eventBody.includes("@You") && !eventBody.includes("@user") && !eventBody.includes("@Board")) continue;
}
notify({ title: threadTitle, body: eventBody });
break;
}
}
export type { ThreadIntegrationDeps } from "./thread-helpers";
export function createThreadIntegration({ db, logger, emitEvent, publishDomainChanged, notify }: ThreadIntegrationDeps) {
return {
@@ -0,0 +1,151 @@
/** Thread Integration — shared helpers and types. */
import type { AppDatabase } from "../database";
import type { ThreadType } from "@shared/types";
export interface ThreadIntegrationDeps {
db: AppDatabase;
logger: { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
emitEvent?: (event: import("@shared/types").DesktopEvent) => void;
publishDomainChanged?: () => void;
notify?: (options: { title: string; body: string }) => void;
}
export function findTaskThread(db: AppDatabase, taskId: string, companyId: string) {
const existing = db.listThreads({
companyId,
type: "task" as ThreadType,
relatedEntityType: "task",
relatedEntityId: taskId,
limit: 1,
});
return existing[0] ?? null;
}
export function ensureTaskThread(db: AppDatabase, taskId: string, companyId: string, title: string) {
const existing = findTaskThread(db, taskId, companyId);
if (existing) return existing;
return db.createThread({
companyId,
title,
type: "task" as ThreadType,
relatedEntityType: "task",
relatedEntityId: taskId,
});
}
export function findApprovalThread(db: AppDatabase, approvalId: string, companyId: string) {
const existing = db.listThreads({
companyId,
type: "approval" as ThreadType,
relatedEntityType: "approval",
relatedEntityId: approvalId,
limit: 1,
});
return existing[0] ?? null;
}
export function ensureApprovalThread(db: AppDatabase, approvalId: string, companyId: string, title: string) {
const existing = findApprovalThread(db, approvalId, companyId);
if (existing) return existing;
return db.createThread({
companyId,
title,
type: "approval" as ThreadType,
relatedEntityType: "approval",
relatedEntityId: approvalId,
});
}
function resolveApprovalAnnouncementChannel(
db: AppDatabase,
companyId: string,
taskId: string | null,
requestedByAgentId: string | null,
): { channel: "department" | "project" | "company"; channelTargetId: string | null } | null {
if (requestedByAgentId) {
try {
const requester = db.getAgent(requestedByAgentId);
if (requester.companyId === companyId && requester.department) {
return { channel: "department", channelTargetId: requester.department };
}
} catch {
// Fall through to task/project/company targeting.
}
}
if (taskId) {
try {
const task = db.getTask(taskId);
if (task.companyId === companyId && task.projectId) {
return { channel: "project", channelTargetId: task.projectId };
}
} catch {
// Fall through to company-wide routing.
}
}
return { channel: "company", channelTargetId: null };
}
export function announceApprovalToBoard(
db: AppDatabase,
approval: {
id: string;
companyId: string;
relatedTaskId: string | null;
requestedByAgentId: string | null;
payloadSummary: string;
impactSummary: string;
},
deps: Pick<ThreadIntegrationDeps, "emitEvent" | "publishDomainChanged">,
): void {
if (!approval.requestedByAgentId || !deps.emitEvent || !deps.publishDomainChanged) return;
const target = resolveApprovalAnnouncementChannel(
db,
approval.companyId,
approval.relatedTaskId,
approval.requestedByAgentId,
);
if (!target) return;
const messageId = db.sendAgentMessage({
companyId: approval.companyId,
fromAgentId: approval.requestedByAgentId,
channel: target.channel,
channelTargetId: target.channelTargetId,
subject: approval.payloadSummary,
body: `@Board ${approval.impactSummary || approval.payloadSummary}`,
priority: "urgent",
senderKind: "agent",
});
const message = db.getAgentMessage(messageId, approval.companyId);
if (message) {
deps.emitEvent({ type: "new-message", message });
}
deps.publishDomainChanged();
}
/** Check subscriber notifyOn preferences and fire desktop notification. */
export function notifyThreadSubscribers(
db: AppDatabase,
threadId: string,
threadTitle: string,
eventBody: string,
notify?: (options: { title: string; body: string }) => void,
): void {
if (!notify) return;
const subscriptions = db.listSubscriptions(threadId);
for (const sub of subscriptions) {
if (sub.subscriberType !== "user") continue;
if (sub.notifyOn === "none") continue;
if (sub.notifyOn === "mentions") {
if (!eventBody.includes("@You") && !eventBody.includes("@user") && !eventBody.includes("@Board")) continue;
}
notify({ title: threadTitle, body: eventBody });
break;
}
}
File renamed without changes.
+16 -11
View File
@@ -78,10 +78,12 @@ export async function handleWorkerMessage(
const now = Date.now();
const lastRenewal = leaseRenewalMap.get(event.runId) ?? 0;
if (now - lastRenewal >= LEASE_RENEWAL_INTERVAL_MS) {
const run = db.listSnapshot().runs.find((r) => r.id === event.runId);
if (run) {
try {
const run = db.getRun(event.runId);
db.renewTaskLease(run.taskId, run.id);
leaseRenewalMap.set(event.runId, now);
} catch {
// Run not found — skip lease renewal
}
}
return;
@@ -102,14 +104,16 @@ export async function handleWorkerMessage(
});
}
if (event.status === "running") {
const run = db.listSnapshot().runs.find((entry) => entry.id === event.runId);
if (run?.workspaceId && !db.claimWorkspaceLock(run.workspaceId, run.id)) {
logger.error(`[workspace] Failed to claim lock for workspace ${run.workspaceId} when run ${run.id} entered running state. Cancelling run.`);
worker?.postMessage({ type: "cancel-run", runId: run.id });
}
if (run) {
try {
const run = db.getRun(event.runId);
if (run.workspaceId && !db.claimWorkspaceLock(run.workspaceId, run.id)) {
logger.error(`[workspace] Failed to claim lock for workspace ${run.workspaceId} when run ${run.id} entered running state. Cancelling run.`);
worker?.postMessage({ type: "cancel-run", runId: run.id });
}
db.renewTaskLease(run.taskId, run.id);
onRunStarted?.(run.id, run.taskId, run.agentId, run.companyId);
} catch {
// Run not found — skip status handling
}
}
emitEvent(event);
@@ -117,9 +121,10 @@ export async function handleWorkerMessage(
return;
}
const snapshot = db.listSnapshot();
const run = snapshot.runs.find((entry) => entry.id === event.runId);
if (!run) {
let run: ReturnType<typeof db["getRun"]>;
try {
run = db.getRun(event.runId);
} catch {
return;
}