@cgh567/agent 2.4.2 → 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.
Files changed (157) 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.js +19 -0
  9. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  10. package/daemon/adapters/tui_wakeup.js +8 -0
  11. package/daemon/context-enrichment.js +27 -0
  12. package/daemon/daemon-manager.js +1 -1
  13. package/daemon/db/email-infrastructure-migrate.js +192 -0
  14. package/daemon/db/hbo-core-migrate.js +189 -0
  15. package/daemon/helios-api.js +863 -64
  16. package/daemon/helios-company-daemon.js +233 -33
  17. package/daemon/lib/blast-radius-analyzer.js +75 -0
  18. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  19. package/daemon/lib/forensic-log.js +113 -0
  20. package/daemon/lib/goal-research-pipeline.js +644 -0
  21. package/daemon/lib/harada/cascade-judge.js +84 -1
  22. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  23. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  24. package/daemon/lib/hbo-bridge.js +74 -6
  25. package/daemon/lib/headroom-middleware.js +129 -0
  26. package/daemon/lib/headroom-proxy-manager.js +309 -0
  27. package/daemon/lib/hed-engine.js +25 -0
  28. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  29. package/daemon/lib/interpretation-engine.js +92 -0
  30. package/daemon/lib/mental-model-cache.js +96 -0
  31. package/daemon/lib/project-factory.js +47 -0
  32. package/daemon/lib/session-log-reader.js +93 -0
  33. package/daemon/lib/standard-work-bootstrap.js +87 -1
  34. package/daemon/lib/task-completion-processor.js +23 -0
  35. package/daemon/lib/wizard-engine.js +57 -6
  36. package/daemon/package.json +2 -1
  37. package/daemon/routes/agents.js +51 -6
  38. package/daemon/routes/channels.js +116 -2
  39. package/daemon/routes/crm.js +85 -0
  40. package/daemon/routes/dashboard.js +62 -16
  41. package/daemon/routes/dept.js +10 -1
  42. package/daemon/routes/email-triage.js +19 -10
  43. package/daemon/routes/hbo.js +618 -58
  44. package/daemon/routes/hed.js +133 -0
  45. package/daemon/routes/inbox.js +397 -8
  46. package/daemon/routes/project.js +580 -66
  47. package/daemon/routes/routines.js +14 -0
  48. package/daemon/routes/tasks.js +15 -1
  49. package/daemon/schema-apply.js +174 -0
  50. package/daemon/schema-definitions.js +433 -0
  51. package/daemon/schema-migrations-hbo.js +20 -0
  52. package/daemon/schema-migrations-hed.js +18 -0
  53. package/daemon/schema-migrations-proj.js +153 -0
  54. package/extensions/__tests__/codebase-index.test.ts +73 -0
  55. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  56. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  57. package/extensions/context-compaction.ts +104 -76
  58. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  59. package/extensions/cortex/wal-replay.ts +91 -0
  60. package/extensions/email/actions/draft-response.ts +21 -1
  61. package/extensions/email/auth/accounts.ts +5 -11
  62. package/extensions/email/auth/inbox-dog.ts +5 -2
  63. package/extensions/email/backfill.ts +20 -13
  64. package/extensions/email/providers/gmail.ts +164 -0
  65. package/extensions/email/providers/google-calendar.ts +34 -5
  66. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  67. package/extensions/helios-browser/backends/playwright.ts +3 -1
  68. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  69. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  70. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  71. package/extensions/hema-dispatch-v3/index.ts +46 -72
  72. package/extensions/interview/__tests__/server.test.ts +117 -0
  73. package/extensions/lib/helios-root.cjs +46 -0
  74. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  75. package/extensions/warm-tick/warm-tick-maintenance.ts +164 -0
  76. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  77. package/lib/__tests__/crash-fixes.test.ts +49 -0
  78. package/lib/__tests__/hbo-core-store.test.js +238 -0
  79. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  80. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  81. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  82. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  83. package/lib/compression/__tests__/pipeline.test.js +280 -0
  84. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  85. package/lib/compression/dist/server.js +34 -1
  86. package/lib/compression/dist/start-server.js +77 -0
  87. package/lib/event-bus.mts +1 -1
  88. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  89. package/lib/graph-availability.js +62 -0
  90. package/lib/hbo-core-store.compiled.js +834 -0
  91. package/lib/hbo-core-store.js +124 -0
  92. package/lib/hbo-core-store.ts +979 -0
  93. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  94. package/lib/skill-sync.js +6 -1
  95. package/lib/startup-integrity.js +9 -2
  96. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  97. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  98. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  99. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  100. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  101. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  102. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  103. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  104. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  105. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  106. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  107. package/lib/triage-core/classifier.ts +41 -8
  108. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  109. package/lib/triage-core/cos/response-debt.ts +2 -2
  110. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  111. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  112. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  113. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  114. package/lib/triage-core/graph/persistence.ts +1 -1
  115. package/lib/triage-core/graph/schema-v2.ts +2 -0
  116. package/lib/triage-core/graph/schema.cypher +11 -0
  117. package/lib/triage-core/graph/triage-query.ts +1 -1
  118. package/lib/triage-core/learning.ts +15 -20
  119. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  120. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  121. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  122. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  123. package/lib/triage-core/mental-model/key-facts.ts +1 -2
  124. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  125. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  126. package/lib/triage-core/orchestrator.ts +8 -15
  127. package/lib/triage-core/scheduled-sends.ts +39 -2
  128. package/lib/triage-core/signals/comms-style.ts +1 -1
  129. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  130. package/lib/triage-core/signals/favee-type.ts +6 -1
  131. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  132. package/lib/triage-core/signals/personal-importance.ts +1 -1
  133. package/lib/triage-core/signals/referral-chain.ts +0 -1
  134. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  135. package/lib/triage-core/signals/relationship-health.ts +6 -1
  136. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  137. package/lib/triage-core/tournament-runner.js +11 -1
  138. package/lib/triage-core/triage-llm-factory.ts +110 -0
  139. package/lib/triage-core/triage-local-llm.ts +145 -0
  140. package/lib/triage-core/triage-sql-store.ts +337 -0
  141. package/lib/triage-core/types.ts +2 -2
  142. package/lib/unified-graph.atomic.test.ts +52 -0
  143. package/lib/unified-graph.failure-categories.test.ts +55 -0
  144. package/package.json +18 -7
  145. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  146. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  147. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  148. package/skills/helios-bookkeeping/SKILL.md +321 -0
  149. package/skills/helios-briefer/SKILL.md +44 -0
  150. package/skills/helios-client-relations/SKILL.md +322 -0
  151. package/skills/helios-personal-triager/SKILL.md +45 -0
  152. package/skills/helios-recruitment/SKILL.md +317 -0
  153. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  154. package/skills/helios-researcher/SKILL.md +44 -0
  155. package/skills/helios-scheduler/SKILL.md +58 -0
  156. package/skills/helios-tax-analyst/SKILL.md +280 -0
  157. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
@@ -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
  }
@@ -37,6 +37,10 @@ export async function score(
37
37
  const lastMs = new Date(lastInteraction).getTime();
38
38
  const daysSilent = (now - lastMs) / (1000 * 60 * 60 * 24);
39
39
 
40
+ if (isNaN(daysSilent) || daysSilent < 0) {
41
+ return { score: 0, evidence: 'invalid lastInteractionAt date — cannot compute decay' };
42
+ }
43
+
40
44
  const tier = model.dunbarLayer ?? 'recognized';
41
45
  const typicalGap = TIER_TYPICAL_GAP[tier] ?? TIER_TYPICAL_GAP.recognized;
42
46
 
@@ -43,7 +43,12 @@ function deriveFaveeValence(model: PersonMentalModel): number | null {
43
43
  if (!conn) return null;
44
44
  const favee = conn.favee;
45
45
  if (!favee) return null;
46
- const parsed = typeof favee === 'string' ? JSON.parse(favee) : favee;
46
+ let parsed: any = null;
47
+ try {
48
+ parsed = typeof favee === 'string' ? JSON.parse(favee) : favee;
49
+ } catch {
50
+ // SEC: malformed favee JSON — skip this connection's favee data
51
+ }
47
52
  return typeof parsed?.valence === 'number' ? parsed.valence : null;
48
53
  }
49
54
 
@@ -13,10 +13,11 @@
13
13
 
14
14
  import type { TriageItem, PersonMentalModel } from '../types.ts';
15
15
  import os from 'os';
16
+ import { _getDb } from '../triage-sql-store.js';
16
17
 
17
18
  const SELF_ID = process.env.HELIOS_SELF_ID;
18
19
  if (!SELF_ID) {
19
- process.stderr.write('[trajectory-signal] WARNING: HELIOS_SELF_ID not set — all queries will use null selfId and may silently return no rows\n');
20
+ console.warn('[trajectory_signal] HELIOS_SELF_ID is not set — trajectory signal will return neutral 0.5 for all items. Set this env var to enable trajectory scoring.');
20
21
  }
21
22
 
22
23
  function getMg() {
@@ -73,13 +74,47 @@ export async function score(
73
74
  rows = await Promise.race([queryPromise, timeoutPromise]);
74
75
  } catch (e) {
75
76
  if (e instanceof Error && e.message === 'timeout') {
76
- process.stderr.write('[trajectory-signal] query timed out after 2000ms — returning neutral\n');
77
+ console.warn('[trajectory-signal] query timed out after 2000ms — returning neutral');
77
78
  return { score: 0.5, evidence: 'timeout — neutral' };
78
79
  }
79
80
  throw e;
80
81
  }
81
82
 
82
83
  if (!rows || rows.length === 0) {
84
+ // SQL fallback: query triage_knows_edges when Memgraph returns empty
85
+ try {
86
+ const db = _getDb();
87
+ const sqlRow = db.prepare(
88
+ `SELECT composite_strength, prev_composite_strength
89
+ FROM triage_knows_edges
90
+ WHERE src_id = ? AND dst_id = ?
91
+ LIMIT 1`
92
+ ).get(SELF_ID, personId) as { composite_strength: number | null; prev_composite_strength: number | null } | undefined;
93
+
94
+ if (sqlRow && sqlRow.composite_strength != null && sqlRow.prev_composite_strength != null) {
95
+ const sqlCurrent = sqlRow.composite_strength;
96
+ const sqlPrev = sqlRow.prev_composite_strength;
97
+ const delta = sqlCurrent - sqlPrev;
98
+ let sqlScore: number;
99
+ let sqlDirection: string;
100
+ if (delta > 0) {
101
+ sqlScore = Math.min(0.9, 0.5 + delta * 2);
102
+ sqlDirection = 'improving';
103
+ } else if (delta < 0) {
104
+ sqlScore = Math.max(0.1, 0.5 + delta * 2);
105
+ sqlDirection = 'decaying';
106
+ } else {
107
+ sqlScore = 0.5;
108
+ sqlDirection = 'stable';
109
+ }
110
+ return {
111
+ score: Math.round(sqlScore * 10000) / 10000,
112
+ evidence: `sql-fallback direction=${sqlDirection} current=${sqlCurrent.toFixed(3)} prev=${sqlPrev.toFixed(3)} delta=${delta.toFixed(3)}`,
113
+ };
114
+ }
115
+ } catch (sqlErr) {
116
+ console.warn(`[trajectory-signal] sql fallback failed: ${String(sqlErr)}`);
117
+ }
83
118
  return { score: 0.5, evidence: 'no KNOWS edge data — neutral' };
84
119
  }
85
120
 
@@ -136,7 +171,7 @@ export async function score(
136
171
  evidence: `direction=${direction} current=${current.toFixed(3)} prev=${prev.toFixed(3)} ratio=${ratio.toFixed(2)} lastAt=${lastAt}`,
137
172
  };
138
173
  } catch (e) {
139
- process.stderr.write(`[trajectory-signal] query failed: ${String(e)}\n`);
174
+ console.warn(`[trajectory-signal] query failed: ${String(e)}`);
140
175
  return { score: 0.5, evidence: 'query error — neutral' };
141
176
  }
142
177
  }
@@ -9,7 +9,12 @@
9
9
 
10
10
  const path = require('path');
11
11
 
12
+ // Fix 1 + Fix 3: Try @mariozechner/jiti first (installed as transitive dep of cli/),
13
+ // then plain jiti (installed in some environments directly).
14
+ // Both paths are tried so the module loads correctly regardless of which variant
15
+ // is present on the host machine (Windows, macOS, Linux).
12
16
  const JITI_PATHS = [
17
+ path.join(__dirname, '..', '..', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
13
18
  path.join(__dirname, '..', '..', 'node_modules', 'jiti', 'lib', 'jiti.cjs'),
14
19
  ];
15
20
 
@@ -38,9 +43,14 @@ function _load() {
38
43
  } catch (_e) { /* no hook */ }
39
44
 
40
45
  process.stderr.write('[tournament-runner.js] WARNING: could not load tournament-runner.ts — using degraded stubs\n');
41
- _mod = { runTournament: opts => ({ winner: opts && opts.candidates && opts.candidates[0] || null, rankings: [], rounds: 0 }) };
46
+ // Fix 2: degraded stub exports both runTournament and runTournamentAsync so
47
+ // callers that require runTournamentAsync (e.g. pillar-dispatcher.js) get a
48
+ // working function instead of undefined, eliminating the WARN log.
49
+ const _stub = opts => ({ winner: opts && opts.candidates && opts.candidates[0] || null, rankings: [], rounds: 0 });
50
+ _mod = { runTournament: _stub, runTournamentAsync: async opts => _stub(opts) };
42
51
  return _mod;
43
52
  }
44
53
 
45
54
  const loaded = _load();
46
55
  module.exports = loaded;
56
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * triage-llm-factory.ts — 3-tier LLM provider fallback for extraction
3
+ *
4
+ * Tier 1: Cloud provider (if HELIOS_TRIAGE_PROVIDER set + credentials exist)
5
+ * Tier 2: Ollama at localhost:11434 (if reachable)
6
+ * Tier 3: Bundled Qwen3-0.6B via triage-local-llm.ts (C11: extraction only)
7
+ *
8
+ * C11: This factory is for entity extraction ONLY. Draft generation uses pi runtime.
9
+ */
10
+
11
+ import type { ExtractionLLM } from './triage-local-llm.ts';
12
+ import { getLocalExtractionLLM, resolveModelPath } from './triage-local-llm.ts';
13
+ import { existsSync } from 'fs';
14
+
15
+ // ─── Ollama availability check ────────────────────────────────────────────────
16
+
17
+ async function isOllamaAvailable(): Promise<boolean> {
18
+ try {
19
+ const controller = new AbortController();
20
+ const timeout = setTimeout(() => controller.abort(), 1500);
21
+ const res = await fetch('http://localhost:11434/api/tags', { signal: controller.signal });
22
+ clearTimeout(timeout);
23
+ return res.ok;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ // ─── Ollama adapter ───────────────────────────────────────────────────────────
30
+
31
+ function getOllamaAdapter(model = 'qwen2.5:0.5b'): ExtractionLLM {
32
+ return {
33
+ async call({ systemPrompt, userMessage }) {
34
+ const body = JSON.stringify({
35
+ model,
36
+ messages: [
37
+ { role: 'system', content: systemPrompt },
38
+ { role: 'user', content: userMessage },
39
+ ],
40
+ stream: false,
41
+ format: 'json',
42
+ options: { temperature: 0.1, num_predict: 256 },
43
+ });
44
+ const res = await fetch('http://localhost:11434/api/chat', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body,
48
+ signal: AbortSignal.timeout(30_000),
49
+ });
50
+ if (!res.ok) throw new Error(`Ollama error: ${res.status}`);
51
+ const data = await res.json() as any;
52
+ const content = data.message?.content ?? '';
53
+ return { content, inputTokens: 0, outputTokens: 0 };
54
+ },
55
+ };
56
+ }
57
+
58
+ // ─── Cloud provider adapter (Bedrock/Anthropic/OpenAI/Gemini) ────────────────
59
+
60
+ async function getCloudAdapter(): Promise<ExtractionLLM | null> {
61
+ const provider = process.env.HELIOS_TRIAGE_PROVIDER;
62
+ if (!provider) return null;
63
+
64
+ try {
65
+ // Reuse the existing Bedrock/Anthropic client from entity-extractor
66
+ const { createExtractionClient } = await import('./mental-model/bedrock-config.js');
67
+ const client = await createExtractionClient?.(provider);
68
+ if (!client) return null;
69
+ return client;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // ─── Main factory ─────────────────────────────────────────────────────────────
76
+
77
+ let _cachedLLM: ExtractionLLM | null = null;
78
+ let _cachedAt = 0;
79
+ const CACHE_TTL = 60_000; // re-resolve tier every 60s
80
+
81
+ export async function getTriageLLM(): Promise<ExtractionLLM> {
82
+ if (_cachedLLM && Date.now() - _cachedAt < CACHE_TTL) return _cachedLLM;
83
+
84
+ // Tier 1: Cloud provider
85
+ try {
86
+ const cloud = await getCloudAdapter();
87
+ if (cloud) {
88
+ _cachedLLM = cloud;
89
+ _cachedAt = Date.now();
90
+ return cloud;
91
+ }
92
+ } catch { /* fall through */ }
93
+
94
+ // Tier 2: Ollama
95
+ if (await isOllamaAvailable()) {
96
+ const ollamaModel = process.env.HELIOS_TRIAGE_MODEL ?? 'qwen2.5:0.5b';
97
+ _cachedLLM = getOllamaAdapter(ollamaModel);
98
+ _cachedAt = Date.now();
99
+ return _cachedLLM;
100
+ }
101
+
102
+ // Tier 3: Bundled Qwen3-0.6B (C11: extraction only)
103
+ if (!existsSync(resolveModelPath())) {
104
+ process.stderr.write('[triage-llm-factory] Qwen3 model not found — skipping Tier 3\n');
105
+ return null as unknown as ExtractionLLM;
106
+ }
107
+ _cachedLLM = await getLocalExtractionLLM();
108
+ _cachedAt = Date.now();
109
+ return _cachedLLM;
110
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * triage-local-llm.ts — Bundled Qwen3-0.6B inference adapter
3
+ *
4
+ * C11: This module handles ONLY entity extraction for 29 signals.
5
+ * NEVER used for draft generation.
6
+ * C12: EXTRACTION_SCHEMA is the locked interface contract.
7
+ * Any change must coordinate across all 29 signal scorers.
8
+ *
9
+ * Model: Qwen3-0.6B-Q4_K_M.gguf (484 MB, Apache 2.0)
10
+ * Runtime: node-llama-cpp@^3.18.1 (QwenChatWrapper added v3.18.0)
11
+ * Path: process.env.HELIOS_MODEL_PATH ?? userData/models/Qwen3-0.6B-Q4_K_M.gguf
12
+ */
13
+
14
+ import { join } from 'path';
15
+ import { existsSync } from 'fs';
16
+ import { homedir } from 'os';
17
+
18
+ // ─── Locked extraction schema (C12) ──────────────────────────────────────────
19
+
20
+ export const EXTRACTION_SCHEMA = {
21
+ type: 'object',
22
+ properties: {
23
+ urgency_score: { type: 'integer', minimum: 1, maximum: 5 },
24
+ topics: { type: 'array', items: { type: 'string' }, maxItems: 5 },
25
+ questions: { type: 'array', items: { type: 'string' } },
26
+ commitments: { type: 'array', items: { type: 'string' } },
27
+ sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
28
+ key_people: { type: 'array', items: { type: 'string' } },
29
+ },
30
+ required: ['urgency_score', 'topics', 'sentiment'],
31
+ additionalProperties: false,
32
+ } as const;
33
+
34
+ export type ExtractionResult = {
35
+ urgency_score: number;
36
+ topics: string[];
37
+ questions: string[];
38
+ commitments: string[];
39
+ sentiment: 'positive' | 'neutral' | 'negative';
40
+ key_people: string[];
41
+ };
42
+
43
+ // ─── ExtractionLLM interface ─────────────────────────────────────────────────
44
+
45
+ export interface ExtractionLLM {
46
+ call(opts: {
47
+ systemPrompt: string;
48
+ userMessage: string;
49
+ }): Promise<{ content: string; inputTokens: number; outputTokens: number }>;
50
+ }
51
+
52
+ // ─── Model path resolution ────────────────────────────────────────────────────
53
+
54
+ export function resolveModelPath(): string {
55
+ // 1. Explicit env override (set by Electron main process from process.resourcesPath)
56
+ if (process.env.HELIOS_MODEL_PATH && existsSync(process.env.HELIOS_MODEL_PATH)) {
57
+ return process.env.HELIOS_MODEL_PATH;
58
+ }
59
+
60
+ // 2. Electron userData fallback
61
+ const candidates: string[] = [];
62
+ if (process.platform === 'win32') {
63
+ const appData = process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
64
+ candidates.push(join(appData, 'Helios Desktop', 'models', 'Qwen3-0.6B-Q4_K_M.gguf'));
65
+ } else if (process.platform === 'darwin') {
66
+ candidates.push(join(homedir(), 'Library', 'Application Support', 'Helios Desktop', 'models', 'Qwen3-0.6B-Q4_K_M.gguf'));
67
+ } else {
68
+ const xdg = process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share');
69
+ candidates.push(join(xdg, 'Helios Desktop', 'models', 'Qwen3-0.6B-Q4_K_M.gguf'));
70
+ }
71
+
72
+ for (const c of candidates) { if (existsSync(c)) return c; }
73
+ return candidates[0]; // Return primary even if not found — error will be thrown at load time
74
+ }
75
+
76
+ // ─── Singleton model promise (prevents concurrent load races) ────────────────
77
+
78
+ let _modelPromise: Promise<any> | null = null;
79
+ let _context: any = null;
80
+
81
+ async function _loadModel(): Promise<any> {
82
+ if (_modelPromise) return _modelPromise;
83
+
84
+ _modelPromise = (async () => {
85
+ const modelPath = resolveModelPath();
86
+ if (!existsSync(modelPath)) {
87
+ throw new Error(`Qwen3-0.6B model not found at ${modelPath}. Run: node scripts/prepare-qwen3-model.cjs`);
88
+ }
89
+
90
+ const { getLlama, LlamaChatSession, LlamaJsonSchemaGrammar } = await import('node-llama-cpp');
91
+ const llama = await getLlama();
92
+ const model = await llama.loadModel({ modelPath });
93
+ _context = await model.createContext({ contextSize: 2048 });
94
+ // Build the JSON schema grammar once at load time (C12: schema is locked)
95
+ const grammar = new LlamaJsonSchemaGrammar(llama, EXTRACTION_SCHEMA as any);
96
+ return { model, context: _context, LlamaChatSession, grammar };
97
+ })();
98
+
99
+ return _modelPromise;
100
+ }
101
+
102
+ // ─── Local LLM adapter ───────────────────────────────────────────────────────
103
+
104
+ export async function getLocalExtractionLLM(): Promise<ExtractionLLM> {
105
+ return {
106
+ async call({ systemPrompt, userMessage }) {
107
+ const { context, LlamaChatSession, grammar } = await _loadModel();
108
+
109
+ // H9/C3 fix: use QwenChatWrapper (node-llama-cpp v3.18). Qwen3ChatWrapper was renamed
110
+ // back to QwenChatWrapper in this version — Qwen3 models use the same wrapper.
111
+ // enableThinking is disabled via promptSuffix: the wrapper's default handles it.
112
+ let chatWrapperInstance: any;
113
+ try {
114
+ const { QwenChatWrapper } = await import('node-llama-cpp');
115
+ chatWrapperInstance = new QwenChatWrapper();
116
+ } catch {
117
+ // Fallback: try Qwen3ChatWrapper name from future versions
118
+ try {
119
+ const nlc = await import('node-llama-cpp') as any;
120
+ const WrapperCls = nlc.Qwen3ChatWrapper || nlc.QwenChatWrapper;
121
+ chatWrapperInstance = WrapperCls ? new WrapperCls() : undefined;
122
+ } catch {
123
+ chatWrapperInstance = undefined;
124
+ }
125
+ }
126
+
127
+ const session = new LlamaChatSession({
128
+ contextSequence: context.getSequence(),
129
+ chatWrapper: chatWrapperInstance,
130
+ systemPrompt,
131
+ });
132
+
133
+ const inputTokens = Math.ceil((systemPrompt.length + userMessage.length) / 4);
134
+ // C3 fix: use grammar parameter (LlamaJsonSchemaGrammar), not responseFormat (OpenAI idiom)
135
+ const content = await session.prompt(userMessage, {
136
+ grammar, // enforces EXTRACTION_SCHEMA JSON output (C12)
137
+ maxTokens: 256,
138
+ temperature: 0.1,
139
+ });
140
+ const outputTokens = Math.ceil(content.length / 4);
141
+
142
+ return { content, inputTokens, outputTokens };
143
+ },
144
+ };
145
+ }