@cgh567/agent 2.4.1 → 2.4.2

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 (146) hide show
  1. package/bin/helios +0 -0
  2. package/bin/helios-rpc-node-wrapper.cjs +0 -0
  3. package/bin/helios-rpc-wrapper.sh +0 -0
  4. package/daemon/adapters/helios-rpc-adapter.js +47 -25
  5. package/daemon/config/com.familiar.helios-daemon.plist +5 -0
  6. package/daemon/config/helios-daemon.service +4 -0
  7. package/daemon/context-enrichment.js +59 -21
  8. package/daemon/helios-api.js +149 -37
  9. package/daemon/helios-company-daemon.js +516 -124
  10. package/daemon/lib/harada/cascade-judge.js +12 -50
  11. package/daemon/lib/harada/mandala.js +20 -0
  12. package/daemon/lib/harada/pillar-dispatcher.js +1 -1
  13. package/daemon/lib/harada/project-factory.js +7 -2
  14. package/daemon/lib/hbo-bridge.js +31 -12
  15. package/daemon/lib/helios-hitl-host.js +15 -2
  16. package/daemon/lib/hitl-interaction-service.js +0 -0
  17. package/daemon/lib/memgraph-verify.js +38 -33
  18. package/daemon/lib/project-drift-detector.js +7 -17
  19. package/daemon/lib/project-semantic-updater.js +1 -14
  20. package/daemon/routes/channels.js +10 -5
  21. package/daemon/routes/harada-map.js +11 -48
  22. package/daemon/routes/hbo.js +89 -28
  23. package/daemon/routes/hitl.js +0 -0
  24. package/daemon/routes/project.js +4 -3
  25. package/daemon/routes/wizard.js +11 -4
  26. package/daemon/schema-migrations-hitl.js +0 -0
  27. package/extensions/001-tool-output-cap.ts +0 -0
  28. package/extensions/context-compaction.ts +45 -26
  29. package/extensions/cortex/activation-bridge.ts +5 -0
  30. package/extensions/cortex/learn.ts +26 -0
  31. package/extensions/email/backfill.ts +0 -0
  32. package/extensions/helios-governance/analysis/ambiguity.ts +0 -0
  33. package/extensions/helios-governance/analysis/compliance.ts +0 -0
  34. package/extensions/helios-governance/analysis/long-task-detector.ts +0 -0
  35. package/extensions/helios-governance/analysis/output-contract.ts +0 -0
  36. package/extensions/helios-governance/analysis/patterns.ts +0 -0
  37. package/extensions/helios-governance/analysis/preflight.ts +0 -0
  38. package/extensions/helios-governance/analysis/recurring-violations.ts +0 -0
  39. package/extensions/helios-governance/analysis/task-classification.ts +0 -0
  40. package/extensions/helios-governance/analysis/task-intent.ts +0 -0
  41. package/extensions/helios-governance/gates/high-impact.ts +1 -1
  42. package/extensions/helios-governance/handlers/_jiti-require.ts +15 -8
  43. package/extensions/helios-governance/handlers/proxy-test-detector.ts +0 -0
  44. package/extensions/hema-dispatch-v3/graph-memory.ts +10 -0
  45. package/extensions/hema-dispatch-v3/index.ts +59 -40
  46. package/extensions/lib/elo-engine.js +0 -0
  47. package/extensions/lib/elo-engine.test.js +0 -0
  48. package/extensions/memgraph-autostart.ts +13 -0
  49. package/extensions/neuroplastic-eval.ts +0 -0
  50. package/extensions/shadow-loop/index.ts +0 -0
  51. package/lib/brain-v2-budget.js +0 -0
  52. package/lib/brain-v2-circuit-breaker.js +0 -0
  53. package/lib/brain-v2.js +0 -0
  54. package/lib/broker/adaptive-throttle.js +0 -0
  55. package/lib/broker/batch-coalescer.js +0 -0
  56. package/lib/broker/bulkhead.js +0 -0
  57. package/lib/broker/channel-registry.js +0 -0
  58. package/lib/broker/circuit-breaker.js +0 -0
  59. package/lib/broker/evidence-cache.js +0 -0
  60. package/lib/broker/health-monitor.js +0 -0
  61. package/lib/broker/mage-queue.js +0 -0
  62. package/lib/broker/priority-queue.js +0 -0
  63. package/lib/broker/server.js.bak-error2-fix +0 -0
  64. package/lib/broker/session-registry.js +0 -0
  65. package/lib/broker/singleton-timers.js +0 -0
  66. package/lib/broker/types.d.ts +0 -0
  67. package/lib/broker/vegas-limit.js +0 -0
  68. package/lib/compression/dist/ccr-store.js +74 -0
  69. package/lib/compression/dist/content-router.js +115 -0
  70. package/lib/compression/dist/pipeline.js +113 -0
  71. package/lib/compression/dist/server.js +265 -0
  72. package/lib/compression/dist/smart-crusher.js +251 -0
  73. package/lib/context-budget.ts +0 -0
  74. package/lib/context-firewall.js +0 -0
  75. package/lib/crm/integration/triage-bridge.js +0 -0
  76. package/lib/email-utils.ts +0 -0
  77. package/lib/eval/__tests__/preflight-checker.test.ts +0 -0
  78. package/lib/eval/__tests__/task-instruction-parser.test.ts +0 -0
  79. package/lib/eval/__tests__/verifier-runner.test.ts +0 -0
  80. package/lib/eval/index.ts +0 -0
  81. package/lib/eval/preflight-checker.ts +0 -0
  82. package/lib/eval/task-domain-classifier.ts +0 -0
  83. package/lib/eval/task-instruction-parser.ts +0 -0
  84. package/lib/eval/verifier-runner.ts +0 -0
  85. package/lib/event-bus.d.ts +0 -0
  86. package/lib/governance-context-selector.ts +0 -0
  87. package/lib/graph/generate-extension-embeddings.js +0 -0
  88. package/lib/graph/generate-static-embeddings.js +0 -0
  89. package/lib/graph/lib/utils.js +1 -1
  90. package/lib/graph-audit.d.ts +0 -0
  91. package/lib/mesh-circuit-breaker.js +0 -0
  92. package/lib/mission-loop/lesson-extractor.ts +0 -0
  93. package/lib/mission-loop/mental-model-scorer.ts +0 -0
  94. package/lib/mission-loop/occ-detector.ts +0 -0
  95. package/lib/mission-loop/query-variants.ts +0 -0
  96. package/lib/mission-loop/verifier-check.ts +0 -0
  97. package/lib/skill-reference-builder.ts +0 -0
  98. package/lib/telemetry/token-breakdown.ts +0 -0
  99. package/lib/tool-compressor.ts +0 -0
  100. package/lib/triage-core/legal-routing.ts +0 -0
  101. package/lib/triage-core/mental-model/dunbar-classifier.ts +0 -0
  102. package/lib/triage-core/mental-model/enrich-all.ts +0 -0
  103. package/lib/triage-core/mental-model/identity-resolver.ts +0 -0
  104. package/lib/triage-core/mental-model/key-facts.ts +0 -0
  105. package/lib/triage-core/mental-model/model-assembler.ts +0 -0
  106. package/lib/triage-core/orchestrator.ts +0 -0
  107. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -0
  108. package/package.json +10 -4
  109. package/skills/helios-business-operator/services/signals/upwork-signals.js +0 -0
  110. package/skills/talisman-ceo/SKILL.md +23 -25
  111. package/skills/talisman-comms/SKILL.md +5 -5
  112. package/skills/talisman-engineering/SKILL.md +5 -5
  113. package/skills/talisman-finance/SKILL.md +10 -8
  114. package/skills/talisman-marketing/SKILL.md +10 -10
  115. package/skills/talisman-sales/SKILL.md +12 -15
  116. package/skills/talisman-support/SKILL.md +5 -5
  117. package/agents/business/talisman-ceo.md +0 -183
  118. package/agents/business/talisman-comms.md +0 -257
  119. package/agents/business/talisman-cto.md +0 -153
  120. package/agents/business/talisman-finance.md +0 -246
  121. package/agents/business/talisman-marketing.md +0 -240
  122. package/agents/business/talisman-sales.md +0 -242
  123. package/agents/business/talisman-support.md +0 -236
  124. package/daemon/lib/approval-expiry.js +0 -162
  125. package/daemon/lib/blast-radius-analyzer.js +0 -75
  126. package/daemon/lib/domain-bootstrap-orchestrator.js +0 -267
  127. package/daemon/lib/forensic-log.js +0 -113
  128. package/daemon/lib/goal-research-pipeline.js +0 -644
  129. package/daemon/lib/harada/cascade-research-dispatcher.js +0 -261
  130. package/daemon/lib/headroom-middleware.js +0 -167
  131. package/daemon/lib/headroom-proxy-manager.js +0 -623
  132. package/daemon/lib/hed-engine.js +0 -307
  133. package/daemon/lib/mental-model-cache.js +0 -96
  134. package/daemon/lib/project-factory.js +0 -47
  135. package/daemon/lib/session-log-reader.js +0 -93
  136. package/daemon/routes/hed.js +0 -133
  137. package/lib/graph/learning/headroom-learn-bridge.js +0 -215
  138. package/skills/helios-bookkeeping/SKILL.md +0 -321
  139. package/skills/helios-briefer/SKILL.md +0 -44
  140. package/skills/helios-client-relations/SKILL.md +0 -322
  141. package/skills/helios-personal-triager/SKILL.md +0 -45
  142. package/skills/helios-recruitment/SKILL.md +0 -317
  143. package/skills/helios-relationship-nudger/SKILL.md +0 -77
  144. package/skills/helios-researcher/SKILL.md +0 -44
  145. package/skills/helios-scheduler/SKILL.md +0 -58
  146. package/skills/helios-tax-analyst/SKILL.md +0 -280
package/bin/helios CHANGED
File without changes
File without changes
File without changes
@@ -33,11 +33,19 @@ const path = require('path');
33
33
  const fs = require('fs');
34
34
  const { randomUUID } = require('crypto');
35
35
 
36
- const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
37
- const MAX_STDOUT_BUF = 10 * 1024 * 1024; // 10MB
38
-
39
36
  // Resolve helios-agent root relative to this file: daemon/adapters/ → two levels up
40
37
  const HELIOS_ROOT = path.resolve(__dirname, '..', '..');
38
+
39
+ // Lazy hboStore for SQLite-first CostEvent writes (P2-7)
40
+ let _hboStoreRpc = null;
41
+ function _getHboStoreRpc() {
42
+ if (_hboStoreRpc) return _hboStoreRpc;
43
+ try { _hboStoreRpc = require(path.join(HELIOS_ROOT, 'lib', 'hbo-core-store')); } catch (_) {}
44
+ return _hboStoreRpc;
45
+ }
46
+
47
+ const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
48
+ const MAX_STDOUT_BUF = 10 * 1024 * 1024; // 10MB
41
49
  const HELIOS_RPC_BIN = path.join(HELIOS_ROOT, 'bin', 'helios-rpc.js');
42
50
 
43
51
  class HRpcAdapter {
@@ -120,21 +128,22 @@ class HRpcAdapter {
120
128
  skill: skill,
121
129
  wakeReason: config.originKind ?? 'heartbeat_timer',
122
130
  }),
123
- // ── Headroom compression proxy ──────────────────────────────────────────
124
- // Route all pi subprocess LLM calls through the local Headroom proxy.
125
- // CacheAligner stabilizes Anthropic/Bedrock KV-cache prefixes.
126
- // SmartCrusher compresses tool outputs and HEMA recall payloads.
127
- // The proxy URL is read here at call time so it picks up the correct
128
- // address after an auto-restart (restart may change the port).
131
+ // ── Helios Compression Server ───────────────────────────────────────────
132
+ // Inject HEADROOM_PROXY_URL so the context-compaction.ts extension can
133
+ // call the compression server directly from the Pi subprocess.
134
+ //
135
+ // IMPORTANT: We do NOT set ANTHROPIC_BASE_URL that would route all LLM
136
+ // calls through the compression server, which is not a full LLM proxy.
137
+ // Instead, compression happens in the context-compaction.ts extension hook
138
+ // BEFORE the LLM call (compress messages array → Pi sends compressed to Bedrock).
139
+ // This is the correct architecture: compress at assembly time, not at wire time.
129
140
  ...(() => {
130
141
  try {
131
142
  const { HeadroomProxyManager } = require('../lib/headroom-proxy-manager');
132
143
  const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
133
144
  if (baseUrl) {
134
145
  return {
135
- ANTHROPIC_BASE_URL: baseUrl,
136
- OPENAI_BASE_URL: `${baseUrl}/v1`,
137
- HEADROOM_PROXY_URL: baseUrl,
146
+ HEADROOM_PROXY_URL: baseUrl,
138
147
  };
139
148
  }
140
149
  } catch (_) {}
@@ -395,20 +404,33 @@ class HRpcAdapter {
395
404
  onMeta?.({ type: 'agent_end', data: { taskId, textLength: lastAssistantText.length } });
396
405
  closeStdin(child);
397
406
  // Write real Bedrock spend as CostEvent so BudgetEnforcer can track it
398
- if (totalCostCents > 0 && context.mgQuery && config.companyId) {
407
+ if (totalCostCents > 0 && config.companyId) {
399
408
  const ceId = 'ce:rpc:' + taskId + ':' + Date.now();
400
- context.mgQuery(
401
- `MERGE (ce:CostEvent {id: $id})
402
- ON CREATE SET
403
- ce.companyId = $cid,
404
- ce.agentId = $agentId,
405
- ce.costCents = toInteger($costCents),
406
- ce.source = 'helios_rpc',
407
- ce.taskId = $taskId,
408
- ce.createdAt = localdatetime()`,
409
- { id: ceId, cid: config.companyId ?? '', agentId: config.agentId ?? agentId ?? '',
410
- costCents: totalCostCents, taskId }
411
- ).catch(e => onLog?.({ stream: 'stderr', chunk: '[helios-rpc-adapter] CostEvent write failed: ' + e.message, ts: new Date().toISOString() }));
409
+ // SQLite-first CostEvent write (P2-7)
410
+ try {
411
+ const _store = _getHboStoreRpc();
412
+ if (_store && _store.createCostEvent) {
413
+ _store.createCostEvent({
414
+ id: ceId, companyId: config.companyId ?? '', agentId: config.agentId ?? agentId ?? '',
415
+ costCents: totalCostCents, source: 'helios_rpc', taskId, createdAt: Date.now(),
416
+ });
417
+ }
418
+ } catch (_) {}
419
+ // Non-blocking Memgraph projection (fire-and-forget)
420
+ if (context.mgQuery) {
421
+ setImmediate(() => context.mgQuery(
422
+ `MERGE (ce:CostEvent {id: $id})
423
+ ON CREATE SET
424
+ ce.companyId = $cid,
425
+ ce.agentId = $agentId,
426
+ ce.costCents = toInteger($costCents),
427
+ ce.source = 'helios_rpc',
428
+ ce.taskId = $taskId,
429
+ ce.createdAt = localdatetime()`,
430
+ { id: ceId, cid: config.companyId ?? '', agentId: config.agentId ?? agentId ?? '',
431
+ costCents: totalCostCents, taskId }
432
+ ).catch(e => onLog?.({ stream: 'stderr', chunk: '[helios-rpc-adapter] CostEvent write failed: ' + e.message, ts: new Date().toISOString() })));
433
+ }
412
434
  }
413
435
 
414
436
  // F1-3: if child doesn't exit within 30s after agent_end, settle now.
@@ -90,6 +90,11 @@
90
90
  <string>unified</string>
91
91
  <key>HELIOS_ALLOW_SEND</key>
92
92
  <string>0</string>
93
+ <!-- --experimental-sqlite enables node:sqlite built-in (hbo-core-store).
94
+ Stable in Node ≥ 23; required flag in Node 22.
95
+ --max-old-space-size=1024 matches PM2 ecosystem.config.js memory cap (SVC-2 fix). -->
96
+ <key>NODE_OPTIONS</key>
97
+ <string>--experimental-sqlite --max-old-space-size=1024</string>
93
98
  <!-- PATH extended to cover common Node.js install locations:
94
99
  nvm default, volta, homebrew, system. The launch script will
95
100
  source the user's shell profile which adds nvm to PATH. -->
@@ -35,6 +35,10 @@ Environment=TZ=UTC
35
35
  Environment=ROUTING_AUTHORITY=unified
36
36
  Environment=AWS_REGION=us-east-1
37
37
  Environment=MEMGRAPH_BOLT_URL=bolt://127.0.0.1:7687
38
+ # --experimental-sqlite enables node:sqlite built-in (hbo-core-store fallback store).
39
+ # Stable in Node ≥ 23; required flag in Node 22.
40
+ # --max-old-space-size=1024 matches the PM2 ecosystem.config.js memory cap (SVC-1 fix).
41
+ Environment=NODE_OPTIONS=--experimental-sqlite --max-old-space-size=1024
38
42
  # HELIOS_ROOT is resolved by launch-daemon.sh — not hardcoded here
39
43
  # EnvironmentFile uses - prefix (dash) to not fail if file is missing
40
44
  EnvironmentFile=-%h/helios-agent/.env
@@ -4,6 +4,14 @@
4
4
 
5
5
  const { HELIOS_ROOT: _HELIOS_ROOT_CE } = require('./lib/paths');
6
6
 
7
+ // Lazy-require hbo-core-store for SQLite-first business entity reads (P2-11)
8
+ let _hboStoreCE = null;
9
+ function _getHboStoreCE() {
10
+ if (_hboStoreCE) return _hboStoreCE;
11
+ try { _hboStoreCE = require('../lib/hbo-core-store'); } catch (_) {}
12
+ return _hboStoreCE;
13
+ }
14
+
7
15
  /** Marker appended when buildContextBrief truncates an oversized brief. */
8
16
  const CONTEXT_BRIEF_TRUNCATED = '[CONTEXT BRIEF TRUNCATED — exceeded 30k char cap (aligned to adapter delivery limit)]';
9
17
 
@@ -430,19 +438,33 @@ async function buildContextBrief(mgQuery, agentId, taskTitle, companyId, hboBrid
430
438
  } catch (e) { /* non-fatal */ }
431
439
 
432
440
  // 3. Agent's own pending tasks (what else is on their plate)
441
+ // Memgraph primary — SQLite fallback on unavailability
433
442
  try {
434
- const pendingResult = await mgQuery(
435
- `MATCH (t:Task {assigneeAgentId: $aid, companyId: $cid})
436
- WHERE t.status IN ['todo', 'in_progress']
437
- RETURN t.title AS title, t.status AS status, t.priority AS priority
438
- ORDER BY CASE t.priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END
439
- LIMIT 5`,
440
- { aid: agentId, cid: companyId }
441
- );
442
- const pendingRecords = toRecords(pendingResult);
443
- if (pendingRecords.length) {
443
+ let pendingRecords = null;
444
+ try {
445
+ const pendingResult = await mgQuery(
446
+ `MATCH (t:Task {assigneeAgentId: $aid, companyId: $cid})
447
+ WHERE t.status IN ['todo', 'in_progress']
448
+ RETURN t.title AS title, t.status AS status, t.priority AS priority
449
+ ORDER BY CASE t.priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END
450
+ LIMIT 5`,
451
+ { aid: agentId, cid: companyId }
452
+ );
453
+ pendingRecords = toRecords(pendingResult).map(r => ({ _title: r.get('title'), _status: r.get('status'), _priority: r.get('priority') }));
454
+ } catch (_mgErr) {
455
+ // Memgraph unavailable — fall back to SQLite pending tasks
456
+ try {
457
+ const _storeCE = _getHboStoreCE();
458
+ if (_storeCE && _storeCE.getTasksByCompanyStatus) {
459
+ const _rows = _storeCE.getTasksByCompanyStatus(companyId, ['todo', 'in_progress'])
460
+ .filter(t => t.assigneeAgentId === agentId);
461
+ pendingRecords = _rows.map(t => ({ _title: t.title, _status: t.status, _priority: t.priority }));
462
+ }
463
+ } catch (_sqliteErr) { /* SQLite also unavailable — skip pending tasks section */ }
464
+ }
465
+ if (pendingRecords && pendingRecords.length) {
444
466
  const lines = pendingRecords.map(r =>
445
- `- [${r.get('priority') || 'P3'}] ${r.get('title')} (${r.get('status')})`
467
+ `- [${r._priority || 'P3'}] ${r._title} (${r._status})`
446
468
  );
447
469
  sections.push({ id: 'active-tasks', content: `## Your Other Tasks\n${lines.join('\n')}` });
448
470
  }
@@ -502,16 +524,32 @@ async function buildContextBrief(mgQuery, agentId, taskTitle, companyId, hboBrid
502
524
  }
503
525
  } catch (_) { /* non-fatal */ }
504
526
 
505
- // § QUARTERLY OKR
527
+ // § QUARTERLY OKR — Memgraph primary, SQLite fallback on unavailability
506
528
  try {
507
- const okrResult = await mgQuery(
508
- `MATCH (o:QuarterlyOKR {companyId: $cid, status: 'active'})
509
- WHERE o.agentId = $agentId OR o.assigneeAgentId = $agentId
510
- RETURN o.objective AS objective, o.keyResults AS keyResults, o.quarter AS quarter
511
- ORDER BY o.createdAt DESC LIMIT 1`,
512
- { agentId, cid: companyId }
513
- ).catch(() => null);
514
- const okrRec = toRecords(okrResult)?.[0];
529
+ let okrRec = null;
530
+ try {
531
+ const okrResult = await mgQuery(
532
+ `MATCH (o:QuarterlyOKR {companyId: $cid, status: 'active'})
533
+ WHERE o.agentId = $agentId OR o.assigneeAgentId = $agentId
534
+ RETURN o.objective AS objective, o.keyResults AS keyResults, o.quarter AS quarter
535
+ ORDER BY o.createdAt DESC LIMIT 1`,
536
+ { agentId, cid: companyId }
537
+ );
538
+ okrRec = toRecords(okrResult)?.[0] ?? null;
539
+ } catch (_mgErr) {
540
+ // Memgraph unavailable — fall back to SQLite OKR data
541
+ try {
542
+ const _storeOKR = _getHboStoreCE();
543
+ if (_storeOKR && _storeOKR.getOKRsByCompanyType) {
544
+ const _okrs = _storeOKR.getOKRsByCompanyType(companyId, 'quarterly_okr')
545
+ .filter(o => (o.agentId === agentId || o.assigneeAgentId === agentId) && o.status === 'active');
546
+ if (_okrs.length) {
547
+ const o = _okrs[0];
548
+ okrRec = { get: (k) => o[k] ?? null };
549
+ }
550
+ }
551
+ } catch (_sqliteErr) { /* SQLite also unavailable — skip OKR section */ }
552
+ }
515
553
  if (okrRec) {
516
554
  const objective = okrRec.get('objective');
517
555
  const keyResults = okrRec.get('keyResults') || [];
@@ -1736,7 +1774,7 @@ ${lines.join('\n')}` });
1736
1774
  try {
1737
1775
  const excl = JSON.parse(pillarData.exclusions || '[]');
1738
1776
  if (excl.length > 0) exclusionNote = `\nDo NOT: ${excl.slice(0, 3).join(', ')}`;
1739
- } catch (_) {}
1777
+ } catch (_) {}
1740
1778
  sections.push(
1741
1779
  `<north_star_reminder>\n` +
1742
1780
  `Before responding, confirm your answer serves this intent:\n` +
@@ -42,6 +42,7 @@ const fs = require('fs');
42
42
  const path = require('path');
43
43
  const crypto = require('crypto');
44
44
  const { TranscriptStore } = require('./transcript-store');
45
+ const hboStore = require('../lib/hbo-core-store');
45
46
 
46
47
  const TRANSCRIPTS_DIR = path.join(__dirname, 'transcripts');
47
48
  const _transcriptStore = new TranscriptStore(TRANSCRIPTS_DIR);
@@ -280,6 +281,26 @@ async function handleGetTasks(req, res, ctx) {
280
281
  let cypher;
281
282
  const params = { limit, cid: ctx.cid };
282
283
 
284
+ // SQLite-first task list read (P2-9)
285
+ try {
286
+ const _storeTasks = hboStore.getTasksByCompanyStatus
287
+ ? (q.status
288
+ ? hboStore.getTasksByCompanyStatus(ctx.cid, q.status)
289
+ : (() => { try { return require('../lib/hbo-core-store').getTasksByCompanyStatus(ctx.cid, ['todo','in_progress','done','andon_paused','help_pending','cancelled']); } catch(_) { return null; } })()
290
+ )
291
+ : null;
292
+ if (_storeTasks) {
293
+ let tasks = _storeTasks;
294
+ if (q.agentId) tasks = tasks.filter(t => t.assigneeAgentId === q.agentId);
295
+ tasks = tasks
296
+ .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))
297
+ .slice(0, limit)
298
+ .map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, assignee: t.assigneeAgentId, body: t.body ?? null }));
299
+ jsonResponse(res, 200, { tasks, count: tasks.length });
300
+ return;
301
+ }
302
+ } catch (_) {}
303
+
283
304
  if (q.status && q.agentId) {
284
305
  cypher = `MATCH (t:Task {companyId: $cid, status: $status, assigneeAgentId: $agentId})
285
306
  RETURN t.id AS id, t.title AS title, t.status AS status,
@@ -503,6 +524,23 @@ async function handleGetApprovals(req, res, ctx) {
503
524
  let cypher;
504
525
  const params = { limit, cid: ctx.cid };
505
526
 
527
+ // SQLite-first approval list read (P2-9)
528
+ try {
529
+ const _storeApprovals = hboStore.getApprovalsByCompanyStatus
530
+ ? (q.status
531
+ ? hboStore.getApprovalsByCompanyStatus(ctx.cid, q.status)
532
+ : hboStore.getApprovalsByCompanyStatus(ctx.cid, ['pending','approved','rejected','expired'])
533
+ )
534
+ : null;
535
+ if (_storeApprovals) {
536
+ const approvals = _storeApprovals
537
+ .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))
538
+ .slice(0, limit);
539
+ jsonResponse(res, 200, { approvals, count: approvals.length });
540
+ return;
541
+ }
542
+ } catch (_) {}
543
+
506
544
  if (q.status) {
507
545
  cypher = `MATCH (a:Approval {companyId: $cid, status: $status})${returnClause}`;
508
546
  params.status = q.status;
@@ -2848,6 +2886,10 @@ let interpretationRoute = () => false;
2848
2886
  let hedRoute = () => false;
2849
2887
  // D4: Department intelligence page handler — initialized lazily inside startApi()
2850
2888
  let handleGetDepartmentPage = null;
2889
+ // Domain-specific routes — initialized lazily inside startApi()
2890
+ let hitlRoute = null;
2891
+ let channelsRoute = null;
2892
+ let emailInfraRoute = null;
2851
2893
  const { handleOKRRoutes } = require('./routes/okrs');
2852
2894
  const { handleSenseiRoutes } = require('./routes/sensei');
2853
2895
  let suggestionsRoute, suggestionsCron;
@@ -2967,6 +3009,14 @@ async function route(req, res, ctx) {
2967
3009
  return;
2968
3010
  }
2969
3011
  const result = ctx.daemon.registerCompany(String(companyId));
3012
+ // Also update ctx.companies so the tenant isolation check accepts this company
3013
+ // on subsequent requests (the 403 guard checks ctx.companies, not _modulesByCompany).
3014
+ // This is safe: ctx.companies is an array that only grows — no entries are removed.
3015
+ if (!ctx.companies) ctx.companies = [];
3016
+ const cidStr = String(companyId);
3017
+ if (!ctx.companies.find(c => String(c.id) === cidStr)) {
3018
+ try { ctx.companies.push({ id: cidStr, name: cidStr }); } catch (_) { /* frozen array — ignore */ }
3019
+ }
2970
3020
  jsonResponse(res, 200, result);
2971
3021
  } catch (err) {
2972
3022
  jsonResponse(res, 500, { error: `registerCompany failed: ${err.message}` });
@@ -2974,6 +3024,51 @@ async function route(req, res, ctx) {
2974
3024
  return;
2975
3025
  }
2976
3026
 
3027
+ // GET /api/v1/context-brief — build and return the agent context brief.
3028
+ //
3029
+ // Used by harbor tests (test_context_propagation.py) and desktop context panel.
3030
+ // Calls buildContextBrief() from context-enrichment.js — the same function the
3031
+ // daemon calls before every agent dispatch.
3032
+ //
3033
+ // Query params:
3034
+ // agentId (required) — the BusinessAgent id
3035
+ // companyId (optional) — defaults to primary company; checked against allowedCompanyIds
3036
+ // nocache (optional) — if set, bypasses the 5-minute context cache
3037
+ //
3038
+ // Primary path: GET /api/v1/context-brief?agentId=X&companyId=Y
3039
+ // → buildContextBrief(mgQuery, agentId, '', companyId)
3040
+ // → { brief: string, agentId, companyId }
3041
+ if (method === 'GET' && pathname === '/api/v1/context-brief') {
3042
+ try {
3043
+ const agentId = parsedUrl.searchParams.get('agentId');
3044
+ const explicitCid = parsedUrl.searchParams.get('companyId');
3045
+ if (!agentId) {
3046
+ jsonResponse(res, 400, { error: 'agentId required' });
3047
+ return;
3048
+ }
3049
+ if (!explicitCid) {
3050
+ jsonResponse(res, 400, { error: 'companyId required' });
3051
+ return;
3052
+ }
3053
+ const cid = resolvedCid;
3054
+ const { mgQuery } = ctx;
3055
+ if (!mgQuery) {
3056
+ jsonResponse(res, 503, { error: 'Memgraph not available' });
3057
+ return;
3058
+ }
3059
+ const { buildContextBrief, invalidateContextCache } = require('./context-enrichment');
3060
+ // Bust 5-minute context cache when nocache=1 is set (used by tests after seeding)
3061
+ if (parsedUrl.searchParams.get('nocache')) {
3062
+ try { invalidateContextCache(cid); } catch (_) {}
3063
+ }
3064
+ const brief = await buildContextBrief(mgQuery, agentId, '', cid, null);
3065
+ jsonResponse(res, 200, { brief: brief || '', agentId, companyId: cid });
3066
+ } catch (err) {
3067
+ jsonResponse(res, 500, { error: `context-brief failed: ${err.message}` });
3068
+ }
3069
+ return;
3070
+ }
3071
+
2977
3072
  // GET /api/headroom/health — proxy to Headroom compression proxy health endpoint
2978
3073
  // Used by HeliosInfraService (helios-desktop) to show proxy status in Ground Control.
2979
3074
  if (method === 'GET' && pathname === '/api/headroom/health') {
@@ -3163,18 +3258,49 @@ async function route(req, res, ctx) {
3163
3258
  const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
3164
3259
  if (!baseUrl) return _origEnd(chunk, encoding, callback);
3165
3260
 
3166
- const headroomAi = require('headroom-ai');
3167
- const result = await headroomAi.compress(
3168
- [{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'hbo_get', content: body }] }],
3169
- { model: 'claude', baseUrl }
3170
- );
3261
+ // Direct HTTP POST /headroom/compress — no npm package required.
3262
+ // Same pattern as context-compaction.ts and headroom-middleware.js.
3263
+ const _payload = JSON.stringify({
3264
+ messages: [{
3265
+ role: 'user',
3266
+ content: [{
3267
+ type: 'tool_result',
3268
+ tool_use_id: 'hbo_get',
3269
+ content: body,
3270
+ }],
3271
+ }],
3272
+ });
3273
+ const _url = new URL(baseUrl);
3274
+ const result = await new Promise((resolve, reject) => {
3275
+ const http = require('http');
3276
+ const req = http.request(
3277
+ {
3278
+ hostname: _url.hostname,
3279
+ port: parseInt(_url.port || '8787', 10),
3280
+ path: '/headroom/compress',
3281
+ method: 'POST',
3282
+ headers: {
3283
+ 'Content-Type': 'application/json',
3284
+ 'Content-Length': Buffer.byteLength(_payload),
3285
+ },
3286
+ },
3287
+ (res) => {
3288
+ let buf = '';
3289
+ res.on('data', (c) => { buf += c; });
3290
+ res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { reject(new Error('bad json')); } });
3291
+ res.on('error', reject);
3292
+ }
3293
+ );
3294
+ req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); });
3295
+ req.on('error', reject);
3296
+ req.write(_payload);
3297
+ req.end();
3298
+ });
3171
3299
 
3172
3300
  let compressed = body;
3173
3301
  try {
3174
- const c = result?.messages?.[0]?.content;
3175
- const text = typeof c === 'string' ? c
3176
- : Array.isArray(c) ? (c[0]?.text ?? c[0]?.content ?? body) : body;
3177
- compressed = text;
3302
+ const c = result?.messages?.[0]?.content?.[0]?.content;
3303
+ if (c) compressed = c;
3178
3304
  } catch (_) {}
3179
3305
 
3180
3306
  if (result?.ccrHashes?.length) {
@@ -3495,41 +3621,27 @@ async function handleGetDepartmentPageRoute(req, res, ctx, department) {
3495
3621
  }
3496
3622
  }
3497
3623
 
3498
- // Cache miss or stale — regenerate
3624
+ // Cache miss or stale — return 202 immediately and generate async
3499
3625
  if (!handleGetDepartmentPage) {
3500
3626
  jsonResponse(res, 503, { error: 'Department page generator not initialized' });
3501
3627
  return;
3502
3628
  }
3503
3629
 
3504
- const narrative = await handleGetDepartmentPage.generatePage(cid, department);
3630
+ // D5: Return 202 immediately so the frontend can show a loading state.
3631
+ // Generation runs in setImmediate (next event-loop tick) and broadcasts
3632
+ // department:page:ready when complete so the client can refetch.
3633
+ jsonResponse(res, 202, { ok: true, generating: true, department, companyId: cid });
3505
3634
 
3506
- // generatePage returns null on failure (LLM not configured, empty data, etc.).
3507
- // When null, query the DepartmentPage node for its status/error so the desktop
3508
- // can show a specific message (e.g. "AWS credentials required") instead of the
3509
- // generic "no intelligence page available" empty state.
3510
- if (!narrative) {
3511
- let failureError = null;
3635
+ setImmediate(async () => {
3512
3636
  try {
3513
- const failedNode = await mg(
3514
- 'MATCH (dp:DepartmentPage {companyId: $cid, department: $dept}) ' +
3515
- 'WHERE dp.status = \'failed\' RETURN dp.error AS error ORDER BY dp.generatedAt DESC LIMIT 1',
3516
- { cid, dept: department }
3517
- ).catch(() => null);
3518
- const failRow = failedNode?.rows?.[0];
3519
- if (failRow) {
3520
- failureError = Array.isArray(failRow) ? failRow[0] : failRow.error;
3637
+ const narrative = await handleGetDepartmentPage.generatePage(cid, department);
3638
+ if (narrative) {
3639
+ broadcast({ type: 'department:page:ready', companyId: cid, department });
3521
3640
  }
3522
- } catch (_) {}
3523
- jsonResponse(res, 200, {
3524
- department,
3525
- narrative: null,
3526
- error: failureError || 'Generation failed — check daemon logs',
3527
- cached: false,
3528
- });
3529
- return;
3530
- }
3531
-
3532
- jsonResponse(res, 200, { department, narrative, generatedAt: new Date().toISOString(), cached: false });
3641
+ } catch (_genErr) {
3642
+ console.warn('[dept-page] async generation error:', _genErr && _genErr.message);
3643
+ }
3644
+ });
3533
3645
 
3534
3646
  } catch (e) {
3535
3647
  jsonResponse(res, 500, { error: 'Department page generation failed: ' + (e.message || 'unknown error') });
@@ -3753,7 +3865,7 @@ function startApi(mgQuery, config = {}, state = {}) {
3753
3865
  }
3754
3866
 
3755
3867
  // Domain-specific routes: HITL, channel setup, email infrastructure
3756
- let hitlRoute = null, channelsRoute = null, emailInfraRoute = null;
3868
+ hitlRoute = null; channelsRoute = null; emailInfraRoute = null;
3757
3869
  try { hitlRoute = require('./routes/hitl')({ mgQuery }); } catch(e) { log('warn', `hitl route unavailable: ${e.message}`); }
3758
3870
  try { channelsRoute = require('./routes/channels')({ mgQuery }); } catch(e) { log('warn', `channels route unavailable: ${e.message}`); }
3759
3871
  try { emailInfraRoute = require('./routes/email-infrastructure')({ mgQuery }); } catch(e) { log('warn', `emailInfra route unavailable: ${e.message}`); }