@cgh567/agent 2.4.3 → 2.4.5

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.
Files changed (141) hide show
  1. package/agents/business/talisman-ceo.md +183 -0
  2. package/agents/business/talisman-comms.md +257 -0
  3. package/agents/business/talisman-cto.md +153 -0
  4. package/agents/business/talisman-finance.md +246 -0
  5. package/agents/business/talisman-marketing.md +240 -0
  6. package/agents/business/talisman-sales.md +242 -0
  7. package/agents/business/talisman-support.md +236 -0
  8. package/bin/helios-rpc-wrapper.sh +4 -1
  9. package/bin/helios-rpc.js +19 -0
  10. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  11. package/daemon/context-enrichment.js +27 -0
  12. package/daemon/helios-api.js +310 -58
  13. package/daemon/helios-company-daemon.js +179 -53
  14. package/daemon/lib/blast-radius-analyzer.js +75 -0
  15. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  16. package/daemon/lib/forensic-log.js +113 -0
  17. package/daemon/lib/goal-research-pipeline.js +644 -0
  18. package/daemon/lib/harada/cascade-judge.js +84 -1
  19. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  20. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  21. package/daemon/lib/hbo-bridge.js +73 -5
  22. package/daemon/lib/headroom-middleware.js +129 -0
  23. package/daemon/lib/headroom-proxy-manager.js +319 -0
  24. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  25. package/daemon/lib/interpretation-engine.js +92 -0
  26. package/daemon/lib/mental-model-cache.js +96 -0
  27. package/daemon/lib/project-factory.js +47 -0
  28. package/daemon/lib/session-log-reader.js +93 -0
  29. package/daemon/lib/standard-work-bootstrap.js +87 -1
  30. package/daemon/lib/task-completion-processor.js +12 -0
  31. package/daemon/package.json +2 -1
  32. package/daemon/routes/agents.js +51 -6
  33. package/daemon/routes/channels.js +116 -2
  34. package/daemon/routes/crm.js +85 -0
  35. package/daemon/routes/dashboard.js +62 -16
  36. package/daemon/routes/dept.js +10 -1
  37. package/daemon/routes/email-triage.js +19 -10
  38. package/daemon/routes/hbo.js +367 -13
  39. package/daemon/routes/hed.js +133 -0
  40. package/daemon/routes/inbox.js +466 -10
  41. package/daemon/routes/project.js +392 -9
  42. package/daemon/schema-definitions.js +10 -0
  43. package/daemon/schema-migrations-hbo.js +10 -0
  44. package/daemon/schema-migrations-proj.js +22 -0
  45. package/extensions/__tests__/codebase-index.test.ts +73 -0
  46. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  47. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  48. package/extensions/context-compaction.ts +104 -76
  49. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  50. package/extensions/email/actions/draft-response.ts +21 -1
  51. package/extensions/email/auth/accounts.ts +5 -11
  52. package/extensions/email/auth/inbox-dog.ts +5 -2
  53. package/extensions/email/backfill.ts +20 -13
  54. package/extensions/email/providers/gmail.ts +164 -0
  55. package/extensions/email/providers/google-calendar.ts +34 -5
  56. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  57. package/extensions/helios-browser/backends/playwright.ts +3 -1
  58. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  59. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  60. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  61. package/extensions/hema-dispatch-v3/index.ts +33 -65
  62. package/extensions/interview/__tests__/server.test.ts +117 -0
  63. package/extensions/lib/helios-root.cjs +46 -0
  64. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  65. package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
  66. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  67. package/lib/__tests__/crash-fixes.test.ts +49 -0
  68. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  69. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  70. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  71. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  72. package/lib/compression/__tests__/pipeline.test.js +280 -0
  73. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  74. package/lib/compression/dist/server.js +34 -1
  75. package/lib/compression/dist/start-server.js +77 -0
  76. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  77. package/lib/hbo-core-store.ts +71 -0
  78. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  79. package/lib/skill-sync.js +6 -1
  80. package/lib/startup-integrity.js +9 -2
  81. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  82. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  83. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  84. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  85. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  86. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  87. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  88. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  89. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  90. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  91. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  92. package/lib/triage-core/classifier.ts +38 -6
  93. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  94. package/lib/triage-core/cos/response-debt.ts +2 -2
  95. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  96. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  97. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  98. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  99. package/lib/triage-core/graph/persistence.ts +1 -1
  100. package/lib/triage-core/graph/schema-v2.ts +2 -0
  101. package/lib/triage-core/graph/schema.cypher +1 -0
  102. package/lib/triage-core/graph/triage-query.ts +1 -1
  103. package/lib/triage-core/learning.ts +15 -20
  104. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  105. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  106. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  107. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  108. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  109. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  110. package/lib/triage-core/orchestrator.ts +4 -4
  111. package/lib/triage-core/scheduled-sends.ts +39 -2
  112. package/lib/triage-core/signals/comms-style.ts +1 -1
  113. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  114. package/lib/triage-core/signals/favee-type.ts +6 -1
  115. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  116. package/lib/triage-core/signals/personal-importance.ts +1 -1
  117. package/lib/triage-core/signals/referral-chain.ts +0 -1
  118. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  119. package/lib/triage-core/signals/relationship-health.ts +6 -1
  120. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  121. package/lib/triage-core/tournament-runner.js +11 -1
  122. package/lib/triage-core/triage-llm-factory.ts +110 -0
  123. package/lib/triage-core/triage-local-llm.ts +145 -0
  124. package/lib/triage-core/triage-sql-store.ts +337 -0
  125. package/lib/triage-core/types.ts +2 -2
  126. package/lib/unified-graph.atomic.test.ts +52 -0
  127. package/lib/unified-graph.failure-categories.test.ts +55 -0
  128. package/package.json +10 -3
  129. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  130. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  131. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  132. package/skills/helios-bookkeeping/SKILL.md +321 -0
  133. package/skills/helios-briefer/SKILL.md +44 -0
  134. package/skills/helios-client-relations/SKILL.md +322 -0
  135. package/skills/helios-personal-triager/SKILL.md +45 -0
  136. package/skills/helios-recruitment/SKILL.md +317 -0
  137. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  138. package/skills/helios-researcher/SKILL.md +44 -0
  139. package/skills/helios-scheduler/SKILL.md +58 -0
  140. package/skills/helios-tax-analyst/SKILL.md +280 -0
  141. 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, acquaintance: 0.3 };
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
- .replace(/[^A-Za-z0-9 .,'\-]/g, '')
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
- p.dunbarLayer = null
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
- const reader = _reader ? { safeRead: _reader } : getReader();
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('debug', 'hbo_bridge.skip_no_company', { reason: 'companyId not configured' });
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.3, evidence: 'No model available' };
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 present
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
- const parsed = typeof favee === 'string' ? JSON.parse(favee) : favee;
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 { homedir } from 'os';
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(`${homedir()}/helios-agent/lib/safe-memgraph.js`);
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, casual: 0.3 };
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
  }