- applications/iknowyou: new Next.js chat application with persona-aware conversations, briefing API, cohort logic, vLLM streaming, and sidebar navigation - tidal M8: add replication control plane (control.rs), tenant migration state machine (migration.rs), tenant/upgrade coordinators, cluster/fault test harnesses - tidal M8 tests: expand m8p2/m8p3/m8p4 test suites; add m8p5_multitenancy and m8_uat - tidal db: split replication_ops out of db/mod.rs (was 647 lines, now 574) - .claude: add kai-park, kaya-osei, mira-vasquez agents; add aeries-design-architect, aeries-fullstack-engineer, aeries-product-visionary skills - docs: update ROADMAP.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
388 lines
12 KiB
TypeScript
388 lines
12 KiB
TypeScript
import { recallByTag, type SynapRecallMemory } from "./synap";
|
|
import { loadProfile, loadCohortPriors } from "./cohorts";
|
|
import type {
|
|
CommunicationBrief,
|
|
TopicEntry,
|
|
PersonProfile,
|
|
} from "./types";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Signal content parsers — match exact formats from observer.ts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface ParsedTopic {
|
|
topic: string;
|
|
domain: string;
|
|
specificity: "surface" | "intermediate" | "expert";
|
|
deepened: boolean;
|
|
continued: boolean;
|
|
}
|
|
|
|
/** Parse: "topic: distributed systems (engineering, expert) [deepened] [continued]" */
|
|
function parseTopicContent(content: string): ParsedTopic | null {
|
|
const match = content.match(
|
|
/^topic:\s*(.+?)\s*\((\w+),\s*(surface|intermediate|expert)\)/
|
|
);
|
|
if (!match) return null;
|
|
return {
|
|
topic: match[1].trim(),
|
|
domain: match[2].trim(),
|
|
specificity: match[3] as ParsedTopic["specificity"],
|
|
deepened: content.includes("[deepened]"),
|
|
continued: content.includes("[continued]"),
|
|
};
|
|
}
|
|
|
|
interface ParsedStyle {
|
|
formality: number;
|
|
lowercase: boolean;
|
|
jargon: boolean;
|
|
structure: string;
|
|
emoji: boolean;
|
|
}
|
|
|
|
/** Parse: "formality: 0.25, lowercase: true, jargon: false, structure: stream_of_thought, emoji: false" */
|
|
function parseStyleContent(content: string): ParsedStyle | null {
|
|
const formality = content.match(/formality:\s*([\d.]+)/);
|
|
if (!formality) return null;
|
|
return {
|
|
formality: parseFloat(formality[1]),
|
|
lowercase: content.includes("lowercase: true"),
|
|
jargon: content.includes("jargon: true"),
|
|
structure:
|
|
content.match(/structure:\s*(\w+)/)?.[1] ?? "stream_of_thought",
|
|
emoji: content.includes("emoji: true"),
|
|
};
|
|
}
|
|
|
|
interface ParsedDynamics {
|
|
leading: "person" | "system";
|
|
builtOnPrevious: boolean;
|
|
redirected: string | null;
|
|
correctedSystem: boolean;
|
|
}
|
|
|
|
/** Parse: "leading: person, built_on_previous: true, redirected: X, corrected system" */
|
|
function parseDynamicsContent(content: string): ParsedDynamics | null {
|
|
const leading = content.match(/leading:\s*(person|system)/);
|
|
if (!leading) return null;
|
|
const redirectMatch = content.match(/redirected:\s*(.+?)(?:,|$)/);
|
|
return {
|
|
leading: leading[1] as "person" | "system",
|
|
builtOnPrevious: content.includes("built_on_previous: true"),
|
|
redirected: redirectMatch ? redirectMatch[1].trim() : null,
|
|
correctedSystem: content.includes("corrected system"),
|
|
};
|
|
}
|
|
|
|
interface ParsedEngagement {
|
|
sentiment: number;
|
|
direction: string;
|
|
substantive: boolean;
|
|
words: number;
|
|
}
|
|
|
|
/** Parse: "sentiment: 0.78 (positive), substantive: true, words: 42" */
|
|
function parseEngagementContent(content: string): ParsedEngagement | null {
|
|
const sentiment = content.match(/sentiment:\s*([\d.]+)\s*\((\w+)\)/);
|
|
if (!sentiment) return null;
|
|
const words = content.match(/words:\s*(\d+)/);
|
|
return {
|
|
sentiment: parseFloat(sentiment[1]),
|
|
direction: sentiment[2],
|
|
substantive: content.includes("substantive: true"),
|
|
words: words ? parseInt(words[1], 10) : 0,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Section builders
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildTopicsSection(
|
|
memories: SynapRecallMemory[]
|
|
): CommunicationBrief["topics"] {
|
|
const topicMap = new Map<
|
|
string,
|
|
{
|
|
domain: string;
|
|
specificity: "surface" | "intermediate" | "expert";
|
|
deepened: boolean;
|
|
count: number;
|
|
lastSeen: number;
|
|
}
|
|
>();
|
|
|
|
for (let i = 0; i < memories.length; i++) {
|
|
const parsed = parseTopicContent(memories[i].content);
|
|
if (!parsed) continue;
|
|
|
|
const key = parsed.topic.toLowerCase();
|
|
const existing = topicMap.get(key);
|
|
if (existing) {
|
|
existing.count++;
|
|
existing.lastSeen = i;
|
|
if (parsed.deepened) existing.deepened = true;
|
|
// Keep highest specificity
|
|
const specOrder = { surface: 0, intermediate: 1, expert: 2 };
|
|
if (specOrder[parsed.specificity] > specOrder[existing.specificity]) {
|
|
existing.specificity = parsed.specificity;
|
|
}
|
|
} else {
|
|
topicMap.set(key, {
|
|
domain: parsed.domain,
|
|
specificity: parsed.specificity,
|
|
deepened: parsed.deepened,
|
|
count: 1,
|
|
lastSeen: i,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by recency * frequency (higher = hotter)
|
|
const entries = [...topicMap.entries()]
|
|
.map(([topic, data]) => ({
|
|
topic,
|
|
...data,
|
|
score: data.count * (1 + data.lastSeen / memories.length),
|
|
}))
|
|
.sort((a, b) => b.score - a.score);
|
|
|
|
const hot: TopicEntry[] = entries.slice(0, 5).map((e) => ({
|
|
topic: e.topic,
|
|
domain: e.domain,
|
|
frequency: e.count,
|
|
specificity: e.specificity,
|
|
deepened: e.deepened,
|
|
}));
|
|
|
|
// Cold: known topics not in the hot list, sorted by count descending
|
|
const cold: TopicEntry[] = entries.slice(5, 8).map((e) => ({
|
|
topic: e.topic,
|
|
domain: e.domain,
|
|
frequency: e.count,
|
|
specificity: e.specificity,
|
|
deepened: e.deepened,
|
|
}));
|
|
|
|
// Domains by frequency
|
|
const domainCount = new Map<string, number>();
|
|
for (const e of entries) {
|
|
domainCount.set(e.domain, (domainCount.get(e.domain) ?? 0) + e.count);
|
|
}
|
|
const domains = [...domainCount.entries()]
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5)
|
|
.map(([d]) => d);
|
|
|
|
return { hot, cold, domains };
|
|
}
|
|
|
|
function buildStyleSection(
|
|
profile: PersonProfile | null,
|
|
styleMemories: SynapRecallMemory[]
|
|
): CommunicationBrief["style"] {
|
|
// Default from profile
|
|
let formality = profile?.avgFormality ?? 0.5;
|
|
let jargon = (profile?.jargonRate ?? 0) > 0.5;
|
|
let emoji = false;
|
|
let structure = "stream_of_thought";
|
|
let avgWords = profile?.avgWordCount ?? 20;
|
|
|
|
// Override with recent signals (more granular)
|
|
let styleCount = 0;
|
|
let formalitySum = 0;
|
|
|
|
for (const mem of styleMemories) {
|
|
const parsed = parseStyleContent(mem.content);
|
|
if (!parsed) continue;
|
|
styleCount++;
|
|
formalitySum += parsed.formality;
|
|
jargon = parsed.jargon;
|
|
emoji = parsed.emoji;
|
|
structure = parsed.structure;
|
|
}
|
|
|
|
if (styleCount > 0) {
|
|
formality = formalitySum / styleCount;
|
|
}
|
|
|
|
const formalityBand: CommunicationBrief["style"]["formality"] =
|
|
formality < 0.35 ? "casual" : formality > 0.65 ? "formal" : "moderate";
|
|
|
|
const lengthBand: CommunicationBrief["style"]["length"] =
|
|
avgWords < 15 ? "terse" : avgWords > 40 ? "verbose" : "moderate";
|
|
|
|
return { formality: formalityBand, length: lengthBand, structure, usesJargon: jargon, usesEmoji: emoji };
|
|
}
|
|
|
|
function buildPatternsSection(
|
|
profile: PersonProfile | null,
|
|
dynamicsMemories: SynapRecallMemory[],
|
|
engagementMemories: SynapRecallMemory[]
|
|
): CommunicationBrief["patterns"] {
|
|
const leadsConversation = (profile?.leaderRate ?? 0.5) > 0.5;
|
|
|
|
// Deepens topics: check if any dynamics show built_on_previous
|
|
let deepensCount = 0;
|
|
let dynamicsTotal = 0;
|
|
for (const mem of dynamicsMemories) {
|
|
const parsed = parseDynamicsContent(mem.content);
|
|
if (!parsed) continue;
|
|
dynamicsTotal++;
|
|
if (parsed.builtOnPrevious) deepensCount++;
|
|
}
|
|
const deepensTopics = dynamicsTotal > 0 ? deepensCount / dynamicsTotal > 0.4 : false;
|
|
|
|
// Sentiment from engagement signals
|
|
const sentiments: number[] = [];
|
|
for (const mem of engagementMemories) {
|
|
const parsed = parseEngagementContent(mem.content);
|
|
if (parsed) sentiments.push(parsed.sentiment);
|
|
}
|
|
|
|
const avgSentiment =
|
|
sentiments.length > 0
|
|
? sentiments.reduce((a, b) => a + b, 0) / sentiments.length
|
|
: profile?.avgSentiment ?? 0.5;
|
|
|
|
// Trend: compare first half to second half
|
|
let sentimentTrend: CommunicationBrief["patterns"]["sentimentTrend"] = "stable";
|
|
if (sentiments.length >= 4) {
|
|
const mid = Math.floor(sentiments.length / 2);
|
|
const firstHalf = sentiments.slice(0, mid).reduce((a, b) => a + b, 0) / mid;
|
|
const secondHalf =
|
|
sentiments.slice(mid).reduce((a, b) => a + b, 0) / (sentiments.length - mid);
|
|
const delta = secondHalf - firstHalf;
|
|
if (delta > 0.1) sentimentTrend = "warming";
|
|
else if (delta < -0.1) sentimentTrend = "cooling";
|
|
}
|
|
|
|
return { leadsConversation, deepensTopics, avgSentiment, sentimentTrend };
|
|
}
|
|
|
|
function buildObservationsSection(memories: SynapRecallMemory[]): string[] {
|
|
return memories
|
|
.map((m) => m.content)
|
|
.filter((c) => c.length > 0)
|
|
.slice(0, 5);
|
|
}
|
|
|
|
async function buildCohortSection(
|
|
profile: PersonProfile | null
|
|
): Promise<CommunicationBrief["cohortPriors"]> {
|
|
if (!profile || !profile.cohorts.length) {
|
|
return { active: false, weight: 0, priors: [] };
|
|
}
|
|
|
|
const weight = 1 / (1 + profile.interactionCount / 10);
|
|
if (weight < 0.1) {
|
|
return { active: false, weight, priors: [] };
|
|
}
|
|
|
|
const priors = await loadCohortPriors(
|
|
profile.cohorts,
|
|
profile.interactionCount
|
|
);
|
|
|
|
return { active: priors.length > 0, weight, priors };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main assembly
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function flattenMemories(result: {
|
|
memories: {
|
|
vivid: SynapRecallMemory[];
|
|
associated: SynapRecallMemory[];
|
|
reconstructed: SynapRecallMemory[];
|
|
};
|
|
}): SynapRecallMemory[] {
|
|
return [
|
|
...result.memories.vivid,
|
|
...result.memories.associated,
|
|
];
|
|
}
|
|
|
|
export async function assembleBrief(
|
|
personId: string
|
|
): Promise<CommunicationBrief> {
|
|
const start = Date.now();
|
|
|
|
// 5 parallel queries
|
|
const [topicResult, styleResult, dynamicsResult, observationResult, profile] =
|
|
await Promise.all([
|
|
recallByTag(
|
|
"topics discussed",
|
|
["signal:topic", `person:${personId}`],
|
|
20,
|
|
0.2
|
|
).catch(() => null),
|
|
recallByTag(
|
|
"communication style",
|
|
["signal:style", `person:${personId}`],
|
|
10,
|
|
0.2
|
|
).catch(() => null),
|
|
recallByTag(
|
|
"conversation dynamics",
|
|
["signal:dynamics", `person:${personId}`],
|
|
10,
|
|
0.2
|
|
).catch(() => null),
|
|
recallByTag(
|
|
"communication patterns",
|
|
["observation", `person:${personId}`],
|
|
5,
|
|
0.3
|
|
).catch(() => null),
|
|
loadProfile(personId).catch(() => null),
|
|
]);
|
|
|
|
// Also fetch engagement signals for sentiment analysis (parallel with cohort)
|
|
const [engagementResult, cohortPriors] = await Promise.all([
|
|
recallByTag(
|
|
"engagement signals",
|
|
["signal:engagement", `person:${personId}`],
|
|
10,
|
|
0.2
|
|
).catch(() => null),
|
|
buildCohortSection(profile),
|
|
]);
|
|
|
|
const topicMemories = topicResult ? flattenMemories(topicResult) : [];
|
|
const styleMemories = styleResult ? flattenMemories(styleResult) : [];
|
|
const dynamicsMemories = dynamicsResult
|
|
? flattenMemories(dynamicsResult)
|
|
: [];
|
|
const observationMemories = observationResult
|
|
? flattenMemories(observationResult)
|
|
: [];
|
|
const engagementMemories = engagementResult
|
|
? flattenMemories(engagementResult)
|
|
: [];
|
|
|
|
const brief: CommunicationBrief = {
|
|
personId,
|
|
interactionCount: profile?.interactionCount ?? 0,
|
|
assembledAt: Date.now(),
|
|
topics: buildTopicsSection(topicMemories),
|
|
style: buildStyleSection(profile, styleMemories),
|
|
patterns: buildPatternsSection(profile, dynamicsMemories, engagementMemories),
|
|
observations: buildObservationsSection(observationMemories),
|
|
cohortPriors,
|
|
assemblyMs: Date.now() - start,
|
|
};
|
|
|
|
console.log(
|
|
`[brief] assembled for ${personId.slice(0, 8)}… in ${brief.assemblyMs}ms: ` +
|
|
`${brief.topics.hot.length} hot topics, ` +
|
|
`style=${brief.style.formality}, ` +
|
|
`${brief.observations.length} observations, ` +
|
|
`cohort=${brief.cohortPriors.active ? "active" : "inactive"}`
|
|
);
|
|
|
|
return brief;
|
|
}
|