@cgh567/agent 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +290 -45
- package/daemon/helios-company-daemon.js +160 -50
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +12 -0
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +33 -65
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +38 -6
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +1 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +10 -3
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
|
@@ -37,3 +37,81 @@ export const ROLE_INFERENCE_MODEL_ID = validateAndFallback(
|
|
|
37
37
|
export const KEY_FACTS_MODEL_ID = validateAndFallback(
|
|
38
38
|
process.env.KEY_FACTS_MODEL_ID || DEFAULT_KEY_FACTS_MODEL, 'KEY_FACTS_MODEL_ID', DEFAULT_KEY_FACTS_MODEL
|
|
39
39
|
);
|
|
40
|
+
|
|
41
|
+
// Phase 8B: Triage provider config
|
|
42
|
+
export function getTriageProviderConfig(): { provider: string; model: string } {
|
|
43
|
+
// Read from settings.json triage block if available
|
|
44
|
+
try {
|
|
45
|
+
const settingsPath = process.env.HELIOS_SETTINGS_PATH ?? require('path').join(require('os').homedir(), '.helios', 'settings.json');
|
|
46
|
+
if (require('fs').existsSync(settingsPath)) {
|
|
47
|
+
const settings = JSON.parse(require('fs').readFileSync(settingsPath, 'utf-8'));
|
|
48
|
+
if (settings?.triage?.provider) {
|
|
49
|
+
return { provider: settings.triage.provider, model: settings.triage.model ?? 'auto' };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch { /* fall through to env vars */ }
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
provider: process.env.HELIOS_TRIAGE_PROVIDER ?? 'local',
|
|
56
|
+
model: process.env.HELIOS_TRIAGE_MODEL ?? 'Qwen3-0.6B-Q4_K_M.gguf',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const TRIAGE_PROVIDER = process.env.HELIOS_TRIAGE_PROVIDER ?? 'local';
|
|
61
|
+
export const TRIAGE_MODEL = process.env.HELIOS_TRIAGE_MODEL ?? 'Qwen3-0.6B-Q4_K_M.gguf';
|
|
62
|
+
|
|
63
|
+
// H10 fix: provide createExtractionClient so triage-llm-factory Tier 1 (cloud) path works.
|
|
64
|
+
// Returns an ExtractionLLM adapter backed by the configured Bedrock/Anthropic/OpenAI client.
|
|
65
|
+
export async function createExtractionClient(provider: string): Promise<import('../triage-local-llm.ts').ExtractionLLM | null> {
|
|
66
|
+
try {
|
|
67
|
+
switch (provider) {
|
|
68
|
+
case 'amazon-bedrock': {
|
|
69
|
+
const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime');
|
|
70
|
+
const client = new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
|
|
71
|
+
return {
|
|
72
|
+
async call({ systemPrompt, userMessage }) {
|
|
73
|
+
const body = JSON.stringify({
|
|
74
|
+
anthropic_version: 'bedrock-2023-05-31',
|
|
75
|
+
max_tokens: 256,
|
|
76
|
+
temperature: 0.1,
|
|
77
|
+
system: systemPrompt,
|
|
78
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
79
|
+
});
|
|
80
|
+
const cmd = new InvokeModelCommand({
|
|
81
|
+
modelId: EXTRACTION_MODEL_ID,
|
|
82
|
+
contentType: 'application/json',
|
|
83
|
+
accept: 'application/json',
|
|
84
|
+
body,
|
|
85
|
+
});
|
|
86
|
+
const response = await client.send(cmd);
|
|
87
|
+
const parsed = JSON.parse(new TextDecoder().decode(response.body));
|
|
88
|
+
const content = parsed?.content?.[0]?.text ?? '';
|
|
89
|
+
const inputTokens = parsed?.usage?.input_tokens ?? 0;
|
|
90
|
+
const outputTokens = parsed?.usage?.output_tokens ?? 0;
|
|
91
|
+
return { content, inputTokens, outputTokens };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
case 'anthropic': {
|
|
96
|
+
const Anthropic = require('@anthropic-ai/sdk').default ?? require('@anthropic-ai/sdk');
|
|
97
|
+
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
98
|
+
return {
|
|
99
|
+
async call({ systemPrompt, userMessage }) {
|
|
100
|
+
const response = await client.messages.create({
|
|
101
|
+
model: 'claude-haiku-4-5-20251001',
|
|
102
|
+
max_tokens: 256, temperature: 0.1,
|
|
103
|
+
system: systemPrompt,
|
|
104
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
105
|
+
});
|
|
106
|
+
const content = response.content?.[0]?.text ?? '';
|
|
107
|
+
return { content, inputTokens: response.usage?.input_tokens ?? 0, outputTokens: response.usage?.output_tokens ?? 0 };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -251,7 +251,7 @@ async function queryRelationshipStrength(personId: string): Promise<number> {
|
|
|
251
251
|
`, { selfId: process.env.HELIOS_SELF_ID || '__SELF_ID_NOT_SET__', pid: personId });
|
|
252
252
|
if (!result?.[0]) return 0.3;
|
|
253
253
|
const { layer, msgs, health } = result[0];
|
|
254
|
-
const layerScore: Record<string, number> = { intimate: 1.0, close: 0.8, friend: 0.6,
|
|
254
|
+
const layerScore: Record<string, number> = { intimate: 1.0, close: 0.8, friend: 0.6, active: 0.4, recognized: 0.2 };
|
|
255
255
|
const base = (layerScore[layer as string] ?? 0.2) * 0.6 + Math.min(1.0, Number(msgs || 0) / 500) * 0.4;
|
|
256
256
|
return health === 'UNHEALTHY' ? base * 0.7 : base;
|
|
257
257
|
} catch { return 0.5; }
|
|
@@ -176,6 +176,10 @@ export function setExtractionLLM(llm: ExtractionLLM): void {
|
|
|
176
176
|
_llm = llm;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Phase 8: Lazy triage LLM factory — cloud → Ollama → bundled Qwen3 (C11: extraction only)
|
|
180
|
+
let _triageLLMOverride: any = null;
|
|
181
|
+
export function setExtractionLLMOverride(llm: any): void { _triageLLMOverride = llm; }
|
|
182
|
+
|
|
179
183
|
// Pricing as of 2026-06 for claude-3-5-sonnet cross-region inference via Bedrock.
|
|
180
184
|
// Override via env: BEDROCK_INPUT_COST_PER_1K, BEDROCK_OUTPUT_COST_PER_1K
|
|
181
185
|
// Source: https://aws.amazon.com/bedrock/pricing/
|
|
@@ -324,15 +328,27 @@ const EXTRACTION_TOOL = {
|
|
|
324
328
|
},
|
|
325
329
|
};
|
|
326
330
|
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Prompt sanitization
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
function sanitizeForPrompt(input: string, maxLen = 200): string {
|
|
336
|
+
return input
|
|
337
|
+
.replace(/\[INST\]/gi, '')
|
|
338
|
+
.replace(/\[\/INST\]/gi, '')
|
|
339
|
+
.replace(/SYSTEM:/gi, '')
|
|
340
|
+
.replace(/`/g, '')
|
|
341
|
+
.replace(/<[^>]*>/g, '')
|
|
342
|
+
.slice(0, maxLen);
|
|
343
|
+
}
|
|
344
|
+
|
|
327
345
|
// ---------------------------------------------------------------------------
|
|
328
346
|
// System prompt with few-shot examples
|
|
329
347
|
// ---------------------------------------------------------------------------
|
|
330
348
|
|
|
331
349
|
function buildSystemPrompt(context?: { senderName?: string; platform?: string }): string {
|
|
332
|
-
const safeSenderName = (context?.senderName || '')
|
|
333
|
-
|
|
334
|
-
.slice(0, 100);
|
|
335
|
-
const safePlatform = (context?.platform || '').replace(/[\n\r"'`<>]/g, '').slice(0, 50);
|
|
350
|
+
const safeSenderName = sanitizeForPrompt(context?.senderName || '', 100);
|
|
351
|
+
const safePlatform = sanitizeForPrompt((context?.platform || '').replace(/[\n\r"']/g, ''), 50);
|
|
336
352
|
const senderInfo = safeSenderName ? ` The message sender is "${safeSenderName}".` : '';
|
|
337
353
|
const platformInfo = safePlatform ? ` The platform is ${safePlatform}.` : '';
|
|
338
354
|
|
|
@@ -754,6 +770,37 @@ async function callLLM(systemPrompt: string, userText: string): Promise<{
|
|
|
754
770
|
inputTokens: number;
|
|
755
771
|
outputTokens: number;
|
|
756
772
|
}> {
|
|
773
|
+
// If explicit override set (tests) or HELIOS_TRIAGE_PROVIDER set, use triage-llm-factory
|
|
774
|
+
if (_triageLLMOverride || process.env.HELIOS_TRIAGE_PROVIDER === 'local' || process.env.HELIOS_TRIAGE_PROVIDER === 'ollama' || !process.env.HELIOS_TRIAGE_PROVIDER) {
|
|
775
|
+
const { getTriageLLM } = await import('../triage-llm-factory.js');
|
|
776
|
+
const triageLlm = _triageLLMOverride ?? await getTriageLLM();
|
|
777
|
+
if (!triageLlm) {
|
|
778
|
+
throw new Error('[entity-extractor] No LLM available — all tiers unavailable (cloud/Ollama/Qwen3 all failed)');
|
|
779
|
+
}
|
|
780
|
+
// triage-llm-factory ExtractionLLM uses { userMessage } not { userText } — adapt
|
|
781
|
+
const result = await triageLlm.call({ systemPrompt, userMessage: userText.slice(0, 16000) });
|
|
782
|
+
// Local/Ollama returns plain JSON string — parse and map to extraction shape
|
|
783
|
+
let data: Record<string, unknown>;
|
|
784
|
+
try { data = JSON.parse(result.content); } catch { throw new Error('triage-llm-factory returned non-JSON: ' + result.content.slice(0, 100)); }
|
|
785
|
+
return {
|
|
786
|
+
extraction: {
|
|
787
|
+
messageType: validateMessageType(data.messageType ?? data.message_type),
|
|
788
|
+
urgency: clamp(Number(data.urgency ?? data.urgency_score ?? 0) / (typeof data.urgency_score === 'number' && data.urgency_score > 1 ? 5 : 1), 0, 1),
|
|
789
|
+
sentiment: validateSentiment(data.sentiment),
|
|
790
|
+
topics: validateTopics(Array.isArray(data.topics) && typeof data.topics[0] === 'string' ? (data.topics as string[]).map(t => ({ name: t, category: 'general', sentiment: null })) : data.topics),
|
|
791
|
+
questions: validateQuestions(data.questions),
|
|
792
|
+
commitments: validateCommitments(data.commitments),
|
|
793
|
+
entities: validateEntities(data.entities ?? data.key_people?.map?.((p: string) => ({ name: p, type: 'person', role: null }))),
|
|
794
|
+
lifeEvents: validateLifeEvents(data.lifeEvents ?? data.life_events ?? []),
|
|
795
|
+
answersQuestion: Boolean(data.answersQuestion ?? data.answers_question ?? false),
|
|
796
|
+
referencesCommitment: Boolean(data.referencesCommitment ?? data.references_commitment ?? false),
|
|
797
|
+
extractionFailed: false,
|
|
798
|
+
},
|
|
799
|
+
inputTokens: result.inputTokens,
|
|
800
|
+
outputTokens: result.outputTokens,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
757
804
|
if (_llm) {
|
|
758
805
|
const result = await _llm.call({
|
|
759
806
|
systemPrompt,
|
|
@@ -48,7 +48,7 @@ const _inFlight = new Map<string, Promise<IdentityResolution>>();
|
|
|
48
48
|
// ── Pending creations queue ───────────────────────────────────────────────────
|
|
49
49
|
// Persons queued for background creation (strategy 5 fallback).
|
|
50
50
|
// Declared at module top to eliminate temporal dead zone risk.
|
|
51
|
-
const _pendingCreations: Array<{handle: string; platform: string; displayName: string; personId?: string; _retryCount?: number}> = [];
|
|
51
|
+
const _pendingCreations: Array<{handle: string; platform: string; displayName: string; personId?: string; id?: string; _retryCount?: number}> = [];
|
|
52
52
|
let _pendingFlush = false;
|
|
53
53
|
|
|
54
54
|
function _getCached(handle: string, platform: string): IdentityResolution | undefined {
|
|
@@ -399,7 +399,7 @@ export async function resolveIdentity(
|
|
|
399
399
|
};
|
|
400
400
|
_cache.set(cacheKey, { value: pending, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
401
401
|
|
|
402
|
-
_pendingCreations.push({ handle, platform, displayName: hints?.displayName || handle, personId: personUUID });
|
|
402
|
+
_pendingCreations.push({ handle, platform, displayName: hints?.displayName || handle, personId: personUUID, id: randomUUID() });
|
|
403
403
|
process.stderr.write(JSON.stringify({ level: 'debug', module: 'identity-resolver', message: '[identity-resolver] Pending person queued', handle, platform, personId: personUUID, slug: pendingSlug }) + '\n');
|
|
404
404
|
|
|
405
405
|
resolveDeferred(pending);
|
|
@@ -413,7 +413,7 @@ export async function resolveIdentity(
|
|
|
413
413
|
return resolutionPromise;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
export function getPendingCreations(): Array<{handle: string; platform: string; displayName: string; personId?: string; _retryCount?: number}> {
|
|
416
|
+
export function getPendingCreations(): Array<{handle: string; platform: string; displayName: string; personId?: string; id?: string; _retryCount?: number}> {
|
|
417
417
|
return [..._pendingCreations]; // non-destructive snapshot
|
|
418
418
|
}
|
|
419
419
|
|
|
@@ -436,11 +436,11 @@ export async function flushPendingCreations(): Promise<number> {
|
|
|
436
436
|
p.name = h.displayName,
|
|
437
437
|
p.platform = h.platform,
|
|
438
438
|
p.createdAt = localdatetime(),
|
|
439
|
-
|
|
439
|
+
p.dunbarLayer = null
|
|
440
440
|
SET p.updatedAt = localdatetime()
|
|
441
441
|
WITH p, h
|
|
442
442
|
MERGE (i:Identity {handle: h.handle, platform: h.platform})
|
|
443
|
-
ON CREATE SET i.personId = p.id
|
|
443
|
+
ON CREATE SET i.id = h.id, i.personId = p.id
|
|
444
444
|
ON MATCH SET i.personId = coalesce(i.personId, p.id)
|
|
445
445
|
MERGE (p)-[:HAS_IDENTITY]->(i)
|
|
446
446
|
`, { handles: batch });
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-assembler-sql.ts — SQL fallback for assembleMentalModel()
|
|
3
|
+
*
|
|
4
|
+
* Used when Memgraph is unavailable. Reads from helios.db triage_* tables.
|
|
5
|
+
* Returns a PersonMentalModel with identical shape to the Memgraph version.
|
|
6
|
+
* C7: Memgraph is primary; this is the fallback only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
const _require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
// Import PersonMentalModel type from types
|
|
13
|
+
import type { PersonMentalModel } from '../types.ts';
|
|
14
|
+
import { _getDb } from '../triage-sql-store.ts';
|
|
15
|
+
|
|
16
|
+
export async function assembleMentalModelSQL(personId: string): Promise<PersonMentalModel | null> {
|
|
17
|
+
try {
|
|
18
|
+
const db = _getDb();
|
|
19
|
+
|
|
20
|
+
// Q_PERSON: Core person data
|
|
21
|
+
const person = db.prepare(`
|
|
22
|
+
SELECT id, name, email, total_interactions, last_interaction_at, is_vip,
|
|
23
|
+
page_rank, betweenness, community_id, dunbar_layer,
|
|
24
|
+
trajectory_direction, trajectory_velocity, clustering_coefficient,
|
|
25
|
+
avg_message_length, emoji_rate, formality_score, initiation_ratio,
|
|
26
|
+
recent_inbound_count, avg_weekly_inbound, resilience_level, resilience_score,
|
|
27
|
+
tags, blocked, job_title, company
|
|
28
|
+
FROM triage_persons WHERE id = ?
|
|
29
|
+
`).get(personId) as any;
|
|
30
|
+
|
|
31
|
+
if (!person) return null;
|
|
32
|
+
|
|
33
|
+
// Q_KNOWS: Relationship edges
|
|
34
|
+
const knowsEdges = db.prepare(`
|
|
35
|
+
SELECT k.src_id, k.dst_id, k.strength, k.favee, k.quality_profile, k.health_status,
|
|
36
|
+
k.episode_count, k.last_message_at, k.composite_strength, k.prev_composite_strength,
|
|
37
|
+
p.name AS other_name, p.email AS other_email
|
|
38
|
+
FROM triage_knows_edges k
|
|
39
|
+
JOIN triage_persons p ON (CASE WHEN k.src_id = ? THEN k.dst_id ELSE k.src_id END) = p.id
|
|
40
|
+
WHERE k.src_id = ? OR k.dst_id = ?
|
|
41
|
+
ORDER BY k.composite_strength DESC
|
|
42
|
+
LIMIT 50
|
|
43
|
+
`).all(personId, personId, personId) as any[];
|
|
44
|
+
|
|
45
|
+
// Q_EPISODES: Recent episodes
|
|
46
|
+
const episodes = db.prepare(`
|
|
47
|
+
SELECT id, text, platform, direction, received_at, message_type, urgency, sentiment, subject
|
|
48
|
+
FROM triage_episodes
|
|
49
|
+
WHERE person_id = ?
|
|
50
|
+
ORDER BY received_at DESC
|
|
51
|
+
LIMIT 20
|
|
52
|
+
`).all(personId) as any[];
|
|
53
|
+
|
|
54
|
+
// Q_KEY_FACTS: Key facts
|
|
55
|
+
const keyFacts = db.prepare(`
|
|
56
|
+
SELECT text, confidence, source FROM triage_key_facts
|
|
57
|
+
WHERE person_id = ?
|
|
58
|
+
ORDER BY confidence DESC
|
|
59
|
+
LIMIT 20
|
|
60
|
+
`).all(personId) as any[];
|
|
61
|
+
|
|
62
|
+
// Q_TOPICS: Topics
|
|
63
|
+
const topics = db.prepare(`
|
|
64
|
+
SELECT t.name, pt.strength FROM triage_person_topics pt
|
|
65
|
+
JOIN triage_topics t ON t.id = pt.topic_id
|
|
66
|
+
WHERE pt.person_id = ?
|
|
67
|
+
ORDER BY pt.strength DESC
|
|
68
|
+
LIMIT 10
|
|
69
|
+
`).all(personId) as any[];
|
|
70
|
+
|
|
71
|
+
// Q_GOALS: Active goals (for goal_relevance signal)
|
|
72
|
+
const goals = db.prepare(`
|
|
73
|
+
SELECT id, text, priority, status, category
|
|
74
|
+
FROM triage_goals WHERE status = 'active'
|
|
75
|
+
ORDER BY priority ASC
|
|
76
|
+
LIMIT 20
|
|
77
|
+
`).all() as any[];
|
|
78
|
+
|
|
79
|
+
// Q_QUESTIONS: Open questions involving this person
|
|
80
|
+
const questions = db.prepare(`
|
|
81
|
+
SELECT text FROM triage_questions
|
|
82
|
+
WHERE person_id = ? AND answered = 0
|
|
83
|
+
ORDER BY created_at DESC LIMIT 5
|
|
84
|
+
`).all(personId) as any[];
|
|
85
|
+
|
|
86
|
+
// Q_COMMITMENTS: Open commitments
|
|
87
|
+
const commitments = db.prepare(`
|
|
88
|
+
SELECT text, direction, due_at FROM triage_commitments
|
|
89
|
+
WHERE person_id = ? AND fulfilled = 0
|
|
90
|
+
ORDER BY due_at ASC LIMIT 5
|
|
91
|
+
`).all(personId) as any[];
|
|
92
|
+
|
|
93
|
+
// Q_CONNECTED: 2-hop neighborhood via recursive CTE
|
|
94
|
+
let connectedPeople: any[] = [];
|
|
95
|
+
try {
|
|
96
|
+
connectedPeople = db.prepare(`
|
|
97
|
+
WITH RECURSIVE hop(node_id, depth) AS (
|
|
98
|
+
SELECT ?, 0
|
|
99
|
+
UNION
|
|
100
|
+
SELECT CASE WHEN e.src_id = hop.node_id THEN e.dst_id ELSE e.src_id END, depth + 1
|
|
101
|
+
FROM triage_knows_edges e
|
|
102
|
+
JOIN hop ON (e.src_id = hop.node_id OR e.dst_id = hop.node_id)
|
|
103
|
+
WHERE depth < 2
|
|
104
|
+
)
|
|
105
|
+
SELECT DISTINCT h.node_id, p.name
|
|
106
|
+
FROM hop h
|
|
107
|
+
JOIN triage_persons p ON p.id = h.node_id
|
|
108
|
+
WHERE h.depth = 2 AND h.node_id <> ?
|
|
109
|
+
LIMIT 10
|
|
110
|
+
`).all(personId, personId) as any[];
|
|
111
|
+
} catch {
|
|
112
|
+
// Recursive CTE might fail on very old SQLite — silently degrade to empty
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Q_KNOWS_EDGE (self edge): for trajectory_signal
|
|
116
|
+
const selfKnows = knowsEdges.find((e: any) => e.src_id === personId || e.dst_id === personId);
|
|
117
|
+
|
|
118
|
+
// Assemble the PersonMentalModel shape
|
|
119
|
+
const model: PersonMentalModel = {
|
|
120
|
+
personId: person.id,
|
|
121
|
+
canonicalName: person.name ?? '',
|
|
122
|
+
identities: [],
|
|
123
|
+
summary: null,
|
|
124
|
+
summaryUpdatedAt: null,
|
|
125
|
+
communicationStyle: null,
|
|
126
|
+
responseLatencyAvg: null,
|
|
127
|
+
preferredChannel: null,
|
|
128
|
+
timezone: null,
|
|
129
|
+
totalInteractions: person.total_interactions ?? 0,
|
|
130
|
+
lastInteractionAt: person.last_interaction_at ?? null,
|
|
131
|
+
isVip: Boolean(person.is_vip),
|
|
132
|
+
pageRank: person.page_rank ?? 0,
|
|
133
|
+
betweennessCentrality: person.betweenness ?? 0,
|
|
134
|
+
degreeCentrality: 0,
|
|
135
|
+
communityId: person.community_id ?? null,
|
|
136
|
+
dunbarLayer: person.dunbar_layer ?? undefined,
|
|
137
|
+
trajectoryDirection: person.trajectory_direction ?? undefined,
|
|
138
|
+
trajectoryVelocity: person.trajectory_velocity ?? undefined,
|
|
139
|
+
clusteringCoefficient: person.clustering_coefficient ?? undefined,
|
|
140
|
+
avgMessageLength: person.avg_message_length ?? undefined,
|
|
141
|
+
emojiRate: person.emoji_rate ?? undefined,
|
|
142
|
+
formalityScore: person.formality_score ?? undefined,
|
|
143
|
+
initiationRatio: person.initiation_ratio ?? undefined,
|
|
144
|
+
recentInboundCount: person.recent_inbound_count ?? 0,
|
|
145
|
+
avgWeeklyInbound: person.avg_weekly_inbound ?? 0,
|
|
146
|
+
resilienceLevel: person.resilience_level ?? null,
|
|
147
|
+
resilienceScore: person.resilience_score ?? null,
|
|
148
|
+
tags: (() => { try { return JSON.parse(person.tags || '[]'); } catch { return []; } })(),
|
|
149
|
+
blocked: Boolean(person.blocked),
|
|
150
|
+
jobTitle: person.job_title ?? undefined,
|
|
151
|
+
company: person.company ?? undefined,
|
|
152
|
+
// Relationship data
|
|
153
|
+
relationshipStrength: selfKnows?.composite_strength ?? 0,
|
|
154
|
+
healthStatus: selfKnows?.health_status ?? null,
|
|
155
|
+
qualityProfile: selfKnows?.quality_profile ? (() => { try { return JSON.parse(selfKnows.quality_profile); } catch { return null; } })() : null,
|
|
156
|
+
// Arrays
|
|
157
|
+
recentEpisodes: episodes.map((e: any) => ({
|
|
158
|
+
id: e.id ?? '',
|
|
159
|
+
text: e.text ?? '',
|
|
160
|
+
platform: (e.platform ?? 'email') as any,
|
|
161
|
+
direction: (e.direction ?? 'inbound') as any,
|
|
162
|
+
receivedAt: e.received_at ?? '',
|
|
163
|
+
messageType: (e.message_type ?? 'fyi') as any,
|
|
164
|
+
})),
|
|
165
|
+
activeTopics: [],
|
|
166
|
+
openQuestions: questions.map((q: any) => ({
|
|
167
|
+
id: '',
|
|
168
|
+
text: q.text,
|
|
169
|
+
intent: 'general' as any,
|
|
170
|
+
status: 'open' as const,
|
|
171
|
+
urgency: 0,
|
|
172
|
+
askedAt: '',
|
|
173
|
+
answeredAt: null,
|
|
174
|
+
})),
|
|
175
|
+
openCommitments: commitments.map((c: any) => ({
|
|
176
|
+
id: '',
|
|
177
|
+
text: c.text,
|
|
178
|
+
owner: 'them' as const,
|
|
179
|
+
status: 'open' as const,
|
|
180
|
+
dueDate: c.due_at ?? null,
|
|
181
|
+
createdAt: '',
|
|
182
|
+
completedAt: null,
|
|
183
|
+
direction: c.direction ?? undefined,
|
|
184
|
+
})),
|
|
185
|
+
connectedPeople: connectedPeople.map((p: any) => ({
|
|
186
|
+
personId: p.node_id,
|
|
187
|
+
name: p.name ?? '',
|
|
188
|
+
relationship: '2-hop connection',
|
|
189
|
+
})),
|
|
190
|
+
keyFacts: keyFacts.map((kf: any) => kf.text),
|
|
191
|
+
topics: topics.map((t: any) => t.name),
|
|
192
|
+
goals: goals.map((g: any) => ({ id: g.id, text: g.text, priority: g.priority, status: g.status, category: g.category })),
|
|
193
|
+
} as PersonMentalModel & { keyFacts?: string[]; topics?: string[]; goals?: any[] };
|
|
194
|
+
|
|
195
|
+
return model;
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
console.warn(`[model-assembler-sql] assembleMentalModelSQL(${personId}) failed: ${e.message}`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { PersonMentalModel, Identity, Topic, Question, Commitment, TriagePl
|
|
|
9
9
|
import { computeRelationshipStrength } from './relationship-strength.ts';
|
|
10
10
|
import { assembleContextBrief } from './context-brief.ts';
|
|
11
11
|
import { detectHorsemenForPerson } from './horsemen-detector.ts';
|
|
12
|
+
import { assembleMentalModelSQL } from './model-assembler-sql.ts';
|
|
12
13
|
import { createRequire } from 'module';
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
const { rawWrite: mgWrite } = require('../../../lib/safe-memgraph.js');
|
|
@@ -73,8 +74,21 @@ export async function assembleMentalModel(
|
|
|
73
74
|
_reader?: (cypher: string, params: Record<string, unknown>) => Promise<Record<string, unknown>[]>,
|
|
74
75
|
): Promise<PersonMentalModel> {
|
|
75
76
|
if (!personId) throw new Error('assembleMentalModel: personId is required');
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
// C7: SQL fallback when Memgraph unavailable or explicitly skipped
|
|
79
|
+
if (process.env.HELIOS_SKIP_MEMGRAPH === '1') {
|
|
80
|
+
const sqlModel = await assembleMentalModelSQL(personId);
|
|
81
|
+
if (sqlModel) return sqlModel;
|
|
82
|
+
// Fall through to Memgraph if SQL also fails
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let reader: GraphReader;
|
|
86
|
+
try {
|
|
87
|
+
reader = _reader ? { safeRead: _reader } : getReader();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// C7: Memgraph unavailable — fall back to SQL
|
|
90
|
+
try { return (await assembleMentalModelSQL(personId)) as PersonMentalModel; } catch { throw e; }
|
|
91
|
+
}
|
|
78
92
|
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
79
93
|
|
|
80
94
|
const [
|
|
@@ -336,7 +350,6 @@ export async function assembleMentalModel(
|
|
|
336
350
|
} as PersonMentalModel;
|
|
337
351
|
}
|
|
338
352
|
|
|
339
|
-
|
|
340
353
|
// ---------------------------------------------------------------------------
|
|
341
354
|
// assembleMentalModelByEmail — lookup by email address (B9: T3-18)
|
|
342
355
|
// ---------------------------------------------------------------------------
|
|
@@ -527,7 +527,7 @@ export class TriageOrchestrator {
|
|
|
527
527
|
if (draftedItems.length > 0) {
|
|
528
528
|
const { rawWrite } = require('../safe-memgraph.js');
|
|
529
529
|
const batch = draftedItems.map(item => ({
|
|
530
|
-
id: `draft-${item.id}`,
|
|
530
|
+
id: `draft-${item.id}-${randomUUID().slice(0, 8)}`,
|
|
531
531
|
type: item.platform === 'slack' ? 'slack' : 'email',
|
|
532
532
|
triggeredBy: `auto_draft_${item.priority}`,
|
|
533
533
|
content: JSON.stringify({
|
|
@@ -598,7 +598,7 @@ export class TriageOrchestrator {
|
|
|
598
598
|
};
|
|
599
599
|
const resolvedCompanyId = this.config.companyId ?? process.env.HELIOS_COMPANY_ID ?? null;
|
|
600
600
|
if (!resolvedCompanyId) {
|
|
601
|
-
this.log('
|
|
601
|
+
this.log('error', 'hbo_bridge.skip_no_company', { reason: 'HELIOS_COMPANY_ID is not set — P0/P1 items will NOT create HBO Task or AgentReadySignal nodes. Set HELIOS_COMPANY_ID in .env to enable HBO wiring.' });
|
|
602
602
|
// Skip HBO wiring when no companyId is available
|
|
603
603
|
} else {
|
|
604
604
|
const hboStats = await batchTriageToHBO(
|
|
@@ -1239,7 +1239,7 @@ export class TriageOrchestrator {
|
|
|
1239
1239
|
|
|
1240
1240
|
// Step 4: Batch persist conversations
|
|
1241
1241
|
const conversationBatch = batch.map(m => ({
|
|
1242
|
-
id: `conv:${m.platform}:${m.threadId}`,
|
|
1242
|
+
id: `conv:${m.platform}:${m.threadId || m.id}`,
|
|
1243
1243
|
platform: m.platform,
|
|
1244
1244
|
threadId: m.threadId || '',
|
|
1245
1245
|
isGroup: m.isGroup || false,
|
|
@@ -1271,7 +1271,7 @@ export class TriageOrchestrator {
|
|
|
1271
1271
|
rawId: m.rawId,
|
|
1272
1272
|
labels: m.labels || [],
|
|
1273
1273
|
personId: resolution.personId,
|
|
1274
|
-
conversationId: `conv:${m.platform}:${m.threadId}`,
|
|
1274
|
+
conversationId: `conv:${m.platform}:${m.threadId || m.id}`,
|
|
1275
1275
|
subject: m.subject,
|
|
1276
1276
|
threadId: m.threadId,
|
|
1277
1277
|
};
|
|
@@ -10,7 +10,11 @@ const QUEUE_PATH = join(homedir(), '.pi', 'agent', 'data', 'email-triage', 'send
|
|
|
10
10
|
|
|
11
11
|
export interface ScheduledMessage {
|
|
12
12
|
id: string; to: string; personId: string; platform: 'email' | 'imessage' | 'slack';
|
|
13
|
-
subject?: string; body: string; scheduledAt: string; createdAt: string; status: 'pending' | 'sent' | 'failed';
|
|
13
|
+
subject?: string; body: string; scheduledAt: string; createdAt: string; status: 'pending' | 'sent' | 'failed' | 'cancelled';
|
|
14
|
+
draftId?: string;
|
|
15
|
+
cancelDraft?: boolean;
|
|
16
|
+
accountEmail?: string;
|
|
17
|
+
error?: string;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export async function getOptimalSendTime(personId: string): Promise<Date> {
|
|
@@ -49,7 +53,7 @@ export function scheduleMessage(msg: Omit<ScheduledMessage, 'id' | 'createdAt' |
|
|
|
49
53
|
|
|
50
54
|
export function getPendingMessages(): ScheduledMessage[] { return loadQueue().filter(m => m.status === 'pending'); }
|
|
51
55
|
export function getDueMessages(): ScheduledMessage[] { const now = new Date().toISOString(); return loadQueue().filter(m => m.status === 'pending' && m.scheduledAt <= now); }
|
|
52
|
-
export function markSent(id: string): void { const q = loadQueue(); const m = q.find(x => x.id === id); if (m) { m.status = 'sent'; saveQueue(q); } }
|
|
56
|
+
export function markSent(id: string): void { const q = loadQueue(); const m = q.find(x => x.id === id && x.status === 'pending'); if (m) { m.status = 'sent'; saveQueue(q); } }
|
|
53
57
|
|
|
54
58
|
function loadQueue(): ScheduledMessage[] { try { if (existsSync(QUEUE_PATH)) return JSON.parse(readFileSync(QUEUE_PATH, 'utf-8')); } catch {} return []; }
|
|
55
59
|
function saveQueue(queue: ScheduledMessage[]): void {
|
|
@@ -57,3 +61,36 @@ function saveQueue(queue: ScheduledMessage[]): void {
|
|
|
57
61
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
58
62
|
writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2));
|
|
59
63
|
}
|
|
64
|
+
|
|
65
|
+
export async function executeDueMessages(
|
|
66
|
+
sendDraftFn: (draftId: string) => Promise<{ id: string }>,
|
|
67
|
+
deleteDraftFn: (draftId: string) => Promise<void>,
|
|
68
|
+
): Promise<{ sent: number; failed: number }> {
|
|
69
|
+
const due = getDueMessages();
|
|
70
|
+
let sent = 0; let failed = 0;
|
|
71
|
+
for (const msg of due) {
|
|
72
|
+
try {
|
|
73
|
+
if (msg.draftId) {
|
|
74
|
+
await sendDraftFn(msg.draftId);
|
|
75
|
+
}
|
|
76
|
+
// else: non-draft scheduled send (not used in Phase 6 but keep path open)
|
|
77
|
+
markSent(msg.id);
|
|
78
|
+
sent++;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
const q = loadQueue();
|
|
81
|
+
const m = q.find(x => x.id === msg.id);
|
|
82
|
+
if (m) { m.status = 'failed'; m.error = String(e); saveQueue(q); }
|
|
83
|
+
failed++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { sent, failed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cancelScheduledSend(id: string): boolean {
|
|
90
|
+
const q = loadQueue();
|
|
91
|
+
const m = q.find(x => x.id === id && x.status === 'pending');
|
|
92
|
+
if (!m) return false;
|
|
93
|
+
m.status = 'cancelled';
|
|
94
|
+
saveQueue(q);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
@@ -14,7 +14,7 @@ export async function score(
|
|
|
14
14
|
item: Partial<TriageItem>,
|
|
15
15
|
model: PersonMentalModel | null,
|
|
16
16
|
): Promise<{ score: number; evidence: string }> {
|
|
17
|
-
if (!model) return { score: 0.
|
|
17
|
+
if (!model) return { score: 0.5, evidence: 'no model available' };
|
|
18
18
|
|
|
19
19
|
const parts: string[] = [];
|
|
20
20
|
let styleScore = 0.5; // default neutral
|
|
@@ -36,8 +36,8 @@ export async function score(
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Also include the current item's platform if
|
|
40
|
-
if (item.platform) {
|
|
39
|
+
// Also include the current item's platform if it is within the 48-hour window
|
|
40
|
+
if (item.platform && item.receivedAt && (Date.now() - new Date(item.receivedAt).getTime()) <= WINDOW_MS) {
|
|
41
41
|
channelsInWindow.add(item.platform);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -32,7 +32,12 @@ export async function score(
|
|
|
32
32
|
return { score: 0, evidence: 'no favee data' };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
let parsed: any = null;
|
|
36
|
+
try {
|
|
37
|
+
parsed = typeof favee === 'string' ? JSON.parse(favee) : favee;
|
|
38
|
+
} catch {
|
|
39
|
+
// SEC: malformed favee JSON — skip this connection's favee data
|
|
40
|
+
}
|
|
36
41
|
const type = (parsed?.canonicalType as string) ?? null;
|
|
37
42
|
const boost = type ? (TYPE_BOOST[type] ?? 0.5) : 0;
|
|
38
43
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TriageItem, PersonMentalModel } from '../types.ts';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { _getDb } from '../triage-sql-store.js';
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
5
6
|
|
|
6
7
|
|
|
@@ -25,8 +26,14 @@ export function clearGoalCache(): void {
|
|
|
25
26
|
async function getActiveGoals(): Promise<ActiveGoal[]> {
|
|
26
27
|
if (_goalCache && Date.now() - _goalCache.fetchedAt < CACHE_TTL) return _goalCache.goals;
|
|
27
28
|
|
|
29
|
+
// In vitest context, skip live Memgraph query — tests manage their own data.
|
|
30
|
+
// Pre-fetched _ctx_activeGoals on the model is the preferred test path.
|
|
31
|
+
if (process.env.VITEST === 'true' || process.env.VITEST === '1') {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
try {
|
|
29
|
-
const { rawRead } = require(
|
|
36
|
+
const { rawRead } = require(path.join(__dirname, '../../safe-memgraph.js'));
|
|
30
37
|
const rows = await Promise.race([
|
|
31
38
|
rawRead(`
|
|
32
39
|
MATCH (g:GoalNode) WHERE g.status = 'active'
|
|
@@ -44,6 +51,28 @@ async function getActiveGoals(): Promise<ActiveGoal[]> {
|
|
|
44
51
|
_goalCache = { goals, fetchedAt: Date.now() };
|
|
45
52
|
return goals;
|
|
46
53
|
} catch {
|
|
54
|
+
// SQL fallback: query triage_goals when Memgraph is unavailable or times out
|
|
55
|
+
try {
|
|
56
|
+
const db = _getDb();
|
|
57
|
+
const sqlRows = db.prepare(
|
|
58
|
+
`SELECT text, priority FROM triage_goals WHERE status = 'active'`
|
|
59
|
+
).all() as Array<{ text: string; priority: string }>;
|
|
60
|
+
|
|
61
|
+
if (sqlRows.length > 0) {
|
|
62
|
+
const goals: ActiveGoal[] = sqlRows.map((r) => ({
|
|
63
|
+
text: r.text || '',
|
|
64
|
+
// triage_goals.priority is an integer (lower = higher priority: 0=P0, 1=P1, 2+=P2)
|
|
65
|
+
priority: typeof r.priority === 'number'
|
|
66
|
+
? (r.priority <= 0 ? 'P0' : r.priority === 1 ? 'P1' : 'P2')
|
|
67
|
+
: (r.priority || 'P2'),
|
|
68
|
+
keywords: (r.text || '').toLowerCase().split(/\s+/).filter((w: string) => w.length > 4),
|
|
69
|
+
}));
|
|
70
|
+
_goalCache = { goals, fetchedAt: Date.now() };
|
|
71
|
+
return goals;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// SQL also unavailable — fall through to cache
|
|
75
|
+
}
|
|
47
76
|
return _goalCache?.goals || [];
|
|
48
77
|
}
|
|
49
78
|
}
|
|
@@ -7,7 +7,7 @@ export async function score(
|
|
|
7
7
|
): Promise<{ score: number; evidence: string }> {
|
|
8
8
|
if (!model) return { score: 0.3, evidence: 'unknown person' };
|
|
9
9
|
const tier = model.tier || model.dunbarLayer || '';
|
|
10
|
-
const tierScores: Record<string, number> = { intimate: 0.95, close: 0.8, active: 0.6,
|
|
10
|
+
const tierScores: Record<string, number> = { intimate: 0.95, close: 0.8, friend: 0.65, active: 0.6, recognized: 0.3 };
|
|
11
11
|
const s = tierScores[tier] || 0.4;
|
|
12
12
|
return { score: s, evidence: `${tier || 'unclassified'} contact` };
|
|
13
13
|
}
|
|
@@ -5,7 +5,6 @@ export async function score(
|
|
|
5
5
|
item: Partial<TriageItem>,
|
|
6
6
|
model: PersonMentalModel | null,
|
|
7
7
|
): Promise<{ score: number; evidence: string }> {
|
|
8
|
-
if (!model) return { score: 0, evidence: 'no referral chain data' };
|
|
9
8
|
const hasIntroduction = (item.body || item.snippet || '').toLowerCase().match(/intro|referred|connect you|meet/);
|
|
10
9
|
return { score: hasIntroduction ? 0.6 : 0, evidence: hasIntroduction ? 'referral language detected' : 'no referral signals' };
|
|
11
10
|
}
|