tidaldb/applications/iknowyou/lib/briefing.ts
jordan eca7765e8d fix: heal_region re-delivers missed WAL batches so partitioned followers converge immediately after heal
- Extract redeliver_missed(tx, db, log) helper into cluster_transport.rs
- heal_region now removes partition then immediately ships any missed
  batch-log entries to the healed follower's channel
- await_convergence refactored to call the same helper (no logic change)
- tidal-server: reload_text_index before search in cluster mode
- tidal-server: write_signal returns Result instead of panicking on unknown signal
- tidal-server: leader shows lag_events=0 (writes directly, no receiver thread)
- tidal-server: fix cluster mode error propagation (ServerError::from)
- docs/runbooks/cluster.md: add full cluster operations runbook
- docker/: add Dockerfile for containerised cluster deployment
- README.md: add tidal-server HTTP API getting-started section
- Split oversized source files per CODING_GUIDELINES §9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 11:57:01 -07:00

379 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;
// Aggregate from recent signals — average numerics, majority-vote booleans/strings
let styleCount = 0;
let formalitySum = 0;
let jargonTrue = 0;
let emojiTrue = 0;
const structureCounts = new Map<string, number>();
for (const mem of styleMemories) {
const parsed = parseStyleContent(mem.content);
if (!parsed) continue;
styleCount++;
formalitySum += parsed.formality;
if (parsed.jargon) jargonTrue++;
if (parsed.emoji) emojiTrue++;
structureCounts.set(parsed.structure, (structureCounts.get(parsed.structure) ?? 0) + 1);
}
if (styleCount > 0) {
formality = formalitySum / styleCount;
jargon = jargonTrue / styleCount > 0.5;
emoji = emojiTrue / styleCount > 0.5;
// Most frequent structure wins
let maxCount = 0;
for (const [s, count] of structureCounts) {
if (count > maxCount) { maxCount = count; structure = s; }
}
}
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 recent batch sentiment against profile's running average
let sentimentTrend: CommunicationBrief["patterns"]["sentimentTrend"] = "stable";
if (sentiments.length >= 3 && profile?.avgSentiment !== undefined) {
const delta = avgSentiment - profile.avgSentiment;
if (delta > 0.1) sentimentTrend = "warming";
else if (delta < -0.1) sentimentTrend = "cooling";
}
return { leadsConversation, deepensTopics, avgSentiment, sentimentTrend };
}
/** Raw signal patterns that should never appear in observation content. */
const RAW_SIGNAL_PATTERNS = [
/^formality:\s/,
/^topic:\s/,
/^leading:\s/,
/^sentiment:\s[\d.]+\s*\(/,
/^response latency:/,
];
function buildObservationsSection(memories: SynapRecallMemory[]): string[] {
return memories
.map((m) => m.content)
.filter((c) => c.length > 0 && !RAW_SIGNAL_PATTERNS.some((p) => p.test(c)))
.slice(0, 5);
}
async function buildCohortSection(
profile: PersonProfile | null
): Promise<CommunicationBrief["cohortPriors"]> {
if (!profile || !profile.cohorts.length || profile.interactionCount >= 30) {
return { active: false, weight: 0, priors: [] };
}
const weight = 1 / (1 + profile.interactionCount / 10);
const priors = await loadCohortPriors(
profile.cohorts,
profile.interactionCount
);
return { active: priors.length > 0, weight, priors };
}
// ---------------------------------------------------------------------------
// Main assembly
// ---------------------------------------------------------------------------
/** Flatten vivid + associated tiers (reconstructed excluded — lower confidence). */
function flattenMemories(result: {
memories: {
vivid: SynapRecallMemory[];
associated: SynapRecallMemory[];
reconstructed: SynapRecallMemory[];
};
}): SynapRecallMemory[] {
const vivid = result.memories?.vivid ?? [];
const associated = result.memories?.associated ?? [];
return [...vivid, ...associated];
}
export async function assembleBrief(
personId: string
): Promise<CommunicationBrief> {
const start = Date.now();
// 6 parallel Synap queries + profile load — single wave
const catchSynap = (label: string) => (err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[brief] ${label} failed for ${personId.slice(0, 8)}…: ${msg}`);
return null;
};
const [topicResult, styleResult, dynamicsResult, observationResult, engagementResult, profile] =
await Promise.all([
recallByTag("topics discussed", ["signal:topic", `person:${personId}`], 20, 0.2).catch(catchSynap("topic recall")),
recallByTag("communication style", ["signal:style", `person:${personId}`], 10, 0.2).catch(catchSynap("style recall")),
recallByTag("conversation dynamics", ["signal:dynamics", `person:${personId}`], 10, 0.2).catch(catchSynap("dynamics recall")),
recallByTag("communication patterns", ["observation", `person:${personId}`], 5, 0.3).catch(catchSynap("observation recall")),
recallByTag("engagement signals", ["signal:engagement", `person:${personId}`], 10, 0.2).catch(catchSynap("engagement recall")),
loadProfile(personId).catch(catchSynap("profile load")),
]);
// Cohort priors chain off profile (needs interactionCount)
const cohortPriors = await 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;
}